Add quick replay mode
Allows very quick forwards and backwards jumping at the cost of only replaying entity positions and block updates. Still needs to be backported from 1.12.2 and contains FIXMEs
This commit is contained in:
1143
src/main/java/com/replaymod/replay/FullReplaySender.java
Executable file
1143
src/main/java/com/replaymod/replay/FullReplaySender.java
Executable file
File diff suppressed because it is too large
Load Diff
520
src/main/java/com/replaymod/replay/QuickReplaySender.java
Normal file
520
src/main/java/com/replaymod/replay/QuickReplaySender.java
Normal file
@@ -0,0 +1,520 @@
|
||||
package com.replaymod.replay;
|
||||
|
||||
import com.github.steveice10.mc.protocol.data.game.PlayerListEntry;
|
||||
import com.github.steveice10.mc.protocol.data.game.PlayerListEntryAction;
|
||||
import com.github.steveice10.mc.protocol.data.game.chunk.BlockStorage;
|
||||
import com.github.steveice10.mc.protocol.data.game.chunk.Column;
|
||||
import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position;
|
||||
import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
|
||||
import com.github.steveice10.mc.protocol.data.game.setting.Difficulty;
|
||||
import com.github.steveice10.mc.protocol.data.game.world.WorldType;
|
||||
import com.github.steveice10.mc.protocol.data.game.world.block.BlockChangeRecord;
|
||||
import com.github.steveice10.mc.protocol.data.game.world.block.BlockState;
|
||||
import com.github.steveice10.mc.protocol.packet.ingame.server.ServerPlayerListEntryPacket;
|
||||
import com.github.steveice10.mc.protocol.packet.ingame.server.ServerRespawnPacket;
|
||||
import com.github.steveice10.mc.protocol.packet.ingame.server.entity.ServerEntityDestroyPacket;
|
||||
import com.github.steveice10.mc.protocol.packet.ingame.server.entity.ServerEntityTeleportPacket;
|
||||
import com.github.steveice10.mc.protocol.packet.ingame.server.entity.player.ServerPlayerPositionRotationPacket;
|
||||
import com.github.steveice10.mc.protocol.packet.ingame.server.entity.spawn.ServerSpawnMobPacket;
|
||||
import com.github.steveice10.mc.protocol.packet.ingame.server.entity.spawn.ServerSpawnObjectPacket;
|
||||
import com.github.steveice10.mc.protocol.packet.ingame.server.entity.spawn.ServerSpawnPaintingPacket;
|
||||
import com.github.steveice10.mc.protocol.packet.ingame.server.entity.spawn.ServerSpawnPlayerPacket;
|
||||
import com.github.steveice10.mc.protocol.packet.ingame.server.world.ServerBlockChangePacket;
|
||||
import com.github.steveice10.mc.protocol.packet.ingame.server.world.ServerChunkDataPacket;
|
||||
import com.github.steveice10.mc.protocol.packet.ingame.server.world.ServerMultiBlockChangePacket;
|
||||
import com.github.steveice10.mc.protocol.packet.ingame.server.world.ServerUnloadChunkPacket;
|
||||
import com.google.common.base.Throwables;
|
||||
import com.google.common.collect.ListMultimap;
|
||||
import com.google.common.collect.Multimaps;
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.SettableFuture;
|
||||
import com.replaymod.core.utils.WrappedTimer;
|
||||
import com.replaymod.replaystudio.PacketData;
|
||||
import com.replaymod.replaystudio.io.ReplayInputStream;
|
||||
import com.replaymod.replaystudio.io.ReplayOutputStream;
|
||||
import com.replaymod.replaystudio.replay.ReplayFile;
|
||||
import com.replaymod.replaystudio.studio.ReplayStudio;
|
||||
import com.replaymod.replaystudio.util.Location;
|
||||
import com.replaymod.replaystudio.util.PacketUtils;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufOutputStream;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.ChannelHandlerAdapter;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.network.EnumConnectionState;
|
||||
import net.minecraft.network.EnumPacketDirection;
|
||||
import net.minecraft.network.Packet;
|
||||
import net.minecraft.network.PacketBuffer;
|
||||
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.gameevent.TickEvent;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.NavigableMap;
|
||||
import java.util.TreeMap;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static com.replaymod.core.versions.MCVer.FML_BUS;
|
||||
import static com.replaymod.replay.ReplayModReplay.LOGGER;
|
||||
|
||||
/**
|
||||
* Sends only chunk updates and entity position updates but tries to do so as quickly as possible.
|
||||
* To do so, it performs an initial analysis of the replay, scanning all of its packets and storing entity positions
|
||||
* and chunk states while doing so.
|
||||
* This allows it to later jump to any time by doing a diff from the current time (including backwards jumping).
|
||||
*/
|
||||
@ChannelHandler.Sharable
|
||||
public class QuickReplaySender extends ChannelHandlerAdapter implements ReplaySender {
|
||||
private final Minecraft mc = Minecraft.getMinecraft();
|
||||
|
||||
private final ReplayModReplay mod;
|
||||
private final ReplayFile replayFile;
|
||||
private ChannelHandlerContext ctx;
|
||||
|
||||
private int currentTimeStamp;
|
||||
private double replaySpeed = 1;
|
||||
|
||||
/**
|
||||
* Whether async mode is enabled.
|
||||
* Async mode is emulated by registering an event handler on client tick.
|
||||
*/
|
||||
private boolean asyncMode;
|
||||
private long lastAsyncUpdateTime;
|
||||
|
||||
private ListenableFuture<Void> initPromise;
|
||||
|
||||
private TreeMap<Integer, Collection<TrackedThing>> thingSpawnsT = new TreeMap<>();
|
||||
private ListMultimap<Integer, TrackedThing> thingSpawns = Multimaps.newListMultimap(thingSpawnsT, ArrayList::new);
|
||||
private TreeMap<Integer, Collection<TrackedThing>> thingDespawnsT = new TreeMap<>();
|
||||
private ListMultimap<Integer, TrackedThing> thingDespawns = Multimaps.newListMultimap(thingDespawnsT, ArrayList::new);
|
||||
private List<TrackedThing> activeThings = new LinkedList<>();
|
||||
|
||||
public QuickReplaySender(ReplayModReplay mod, ReplayFile replayFile) {
|
||||
this.mod = mod;
|
||||
this.replayFile = replayFile;
|
||||
}
|
||||
|
||||
public void register() {
|
||||
FML_BUS.register(this);
|
||||
}
|
||||
|
||||
public void unregister() {
|
||||
FML_BUS.unregister(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerAdded(ChannelHandlerContext ctx) {
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
public ListenableFuture<Void> initialize(Consumer<Double> progress) {
|
||||
if (initPromise != null) {
|
||||
return initPromise;
|
||||
}
|
||||
SettableFuture<Void> promise = SettableFuture.create();
|
||||
initPromise = promise;
|
||||
new Thread(() -> {
|
||||
try {
|
||||
long start = System.currentTimeMillis();
|
||||
analyseReplay(progress);
|
||||
LOGGER.info("Initialized quick replay sender in " + (System.currentTimeMillis() - start) + "ms");
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Initializing quick replay sender:", e);
|
||||
mod.getCore().runLater(() -> {
|
||||
mod.getCore().printWarningToChat("Error initializing quick replay sender: %s", e.getLocalizedMessage());
|
||||
promise.setException(e);
|
||||
});
|
||||
return;
|
||||
}
|
||||
mod.getCore().runLater(() -> promise.set(null));
|
||||
}).start();
|
||||
return promise;
|
||||
}
|
||||
|
||||
public void ensureInitialized(Runnable body) {
|
||||
if (initPromise == null) {
|
||||
// TODO progress popup
|
||||
initialize(progress -> {});
|
||||
}
|
||||
Futures.addCallback(initPromise, new FutureCallback<Void>() {
|
||||
@Override
|
||||
public void onSuccess(@Nullable Void result) {
|
||||
body.run();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable t) {
|
||||
// Error already printed by initialize method
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void restart() {
|
||||
activeThings.clear();
|
||||
currentTimeStamp = 0;
|
||||
ctx.fireChannelRead(toMC(new ServerRespawnPacket(0, Difficulty.NORMAL, GameMode.SPECTATOR, WorldType.DEFAULT)));
|
||||
ctx.fireChannelRead(toMC(new ServerPlayerPositionRotationPacket(0, 0, 0, 0, 0, 0)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int currentTimeStamp() {
|
||||
return currentTimeStamp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReplaySpeed(double factor) {
|
||||
if (factor != 0) {
|
||||
if (paused() && asyncMode) {
|
||||
lastAsyncUpdateTime = System.currentTimeMillis(); // TODO test this
|
||||
}
|
||||
this.replaySpeed = factor;
|
||||
}
|
||||
//#if MC>=11200
|
||||
mc.timer.tickLength = WrappedTimer.DEFAULT_MS_PER_TICK / (float) factor;
|
||||
//#else
|
||||
//$$ mc.timer.timerSpeed = (float) factor;
|
||||
//#endif
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getReplaySpeed() {
|
||||
return replaySpeed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAsyncMode() {
|
||||
return asyncMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAsyncMode(boolean async) {
|
||||
if (this.asyncMode == async) return;
|
||||
ensureInitialized(() -> {
|
||||
this.asyncMode = async;
|
||||
if (async) {
|
||||
lastAsyncUpdateTime = System.currentTimeMillis();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSyncModeAndWait() {
|
||||
setAsyncMode(false);
|
||||
// No waiting required, we emulated async mode via tick events
|
||||
}
|
||||
|
||||
@Override
|
||||
public void jumpToTime(int value) {
|
||||
sendPacketsTill(value);
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public void onTick(TickEvent.ClientTickEvent event) {
|
||||
if (event.phase != TickEvent.Phase.START) return;
|
||||
if (!asyncMode) return;
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
long realTimePassed = now - lastAsyncUpdateTime;
|
||||
lastAsyncUpdateTime = now;
|
||||
int replayTimePassed = (int) (realTimePassed * replaySpeed);
|
||||
sendPacketsTill(currentTimeStamp + replayTimePassed);
|
||||
}
|
||||
|
||||
private void analyseReplay(Consumer<Double> progress) {
|
||||
ReplayStudio studio = new ReplayStudio();
|
||||
PacketUtils.registerAllMovementRelated(studio);
|
||||
studio.setParsing(ServerSpawnMobPacket.class, true);
|
||||
studio.setParsing(ServerSpawnObjectPacket.class, true);
|
||||
studio.setParsing(ServerSpawnPaintingPacket.class, true);
|
||||
studio.setParsing(ServerSpawnPlayerPacket.class, true);
|
||||
studio.setParsing(ServerEntityDestroyPacket.class, true);
|
||||
studio.setParsing(ServerChunkDataPacket.class, true);
|
||||
studio.setParsing(ServerUnloadChunkPacket.class, true);
|
||||
studio.setParsing(ServerBlockChangePacket.class, true);
|
||||
studio.setParsing(ServerMultiBlockChangePacket.class, true);
|
||||
studio.setParsing(ServerPlayerListEntryPacket.class, true);
|
||||
|
||||
Map<UUID, PlayerListEntry> playerListEntries = new HashMap<>();
|
||||
Map<Integer, Entity> activeEntities = new HashMap<>();
|
||||
Map<Long, Chunk> activeChunks = new HashMap<>();
|
||||
|
||||
try (ReplayInputStream in = replayFile.getPacketData(studio)) {
|
||||
double duration = replayFile.getMetaData().getDuration();
|
||||
PacketData packetData;
|
||||
while ((packetData = in.readPacket()) != null) {
|
||||
com.github.steveice10.packetlib.packet.Packet packet = packetData.getPacket();
|
||||
int time = (int) packetData.getTime();
|
||||
progress.accept(time / duration);
|
||||
Integer entityId = PacketUtils.getEntityId(packet);
|
||||
if (packet instanceof ServerSpawnMobPacket
|
||||
|| packet instanceof ServerSpawnObjectPacket
|
||||
|| packet instanceof ServerSpawnPaintingPacket) {
|
||||
Entity entity = new Entity(entityId, Collections.singletonList(toMC(packet)));
|
||||
entity.spawnTime = time;
|
||||
thingSpawns.put(time, entity);
|
||||
Entity prev = activeEntities.put(entityId, entity);
|
||||
if (prev != null) {
|
||||
prev.despawnTime = time;
|
||||
thingDespawns.put(time, prev);
|
||||
}
|
||||
} else if (packet instanceof ServerSpawnPlayerPacket) {
|
||||
ServerPlayerListEntryPacket listEntryPacket = new ServerPlayerListEntryPacket(
|
||||
PlayerListEntryAction.ADD_PLAYER,
|
||||
new PlayerListEntry[]{
|
||||
playerListEntries.get(((ServerSpawnPlayerPacket) packet).getUUID())
|
||||
}
|
||||
);
|
||||
Entity entity = new Entity(entityId, Arrays.asList(toMC(listEntryPacket), toMC(packet)));
|
||||
entity.spawnTime = time;
|
||||
thingSpawns.put(time, entity);
|
||||
Entity prev = activeEntities.put(entityId, entity);
|
||||
if (prev != null) {
|
||||
prev.despawnTime = time;
|
||||
thingDespawns.put(time, prev);
|
||||
}
|
||||
} else if (packet instanceof ServerEntityDestroyPacket) {
|
||||
for (int id : ((ServerEntityDestroyPacket) packet).getEntityIds()) {
|
||||
Entity entity = activeEntities.remove(id);
|
||||
if (entity != null) {
|
||||
entity.despawnTime = time;
|
||||
thingDespawns.put(time, entity);
|
||||
}
|
||||
}
|
||||
} else if (packet instanceof ServerChunkDataPacket) {
|
||||
Column column = ((ServerChunkDataPacket) packet).getColumn();
|
||||
Chunk chunk = new Chunk(column);
|
||||
chunk.spawnTime = time;
|
||||
thingSpawns.put(time, chunk);
|
||||
Chunk prev = activeChunks.put(coordToLong(column.getX(), column.getZ()), chunk);
|
||||
if (prev != null) {
|
||||
prev.currentBlockState = null; // free memory because we no longer need it
|
||||
prev.despawnTime = time;
|
||||
thingDespawns.put(time, prev);
|
||||
}
|
||||
} else if (packet instanceof ServerUnloadChunkPacket) {
|
||||
ServerUnloadChunkPacket p = (ServerUnloadChunkPacket) packet;
|
||||
Chunk prev = activeChunks.remove(coordToLong(p.getX(), p.getZ()));
|
||||
if (prev != null) {
|
||||
prev.currentBlockState = null; // free memory because we no longer need it
|
||||
prev.despawnTime = time;
|
||||
thingDespawns.put(time, prev);
|
||||
}
|
||||
} else if (packet instanceof ServerBlockChangePacket || packet instanceof ServerMultiBlockChangePacket) {
|
||||
for (BlockChangeRecord record :
|
||||
packet instanceof ServerBlockChangePacket
|
||||
? new BlockChangeRecord[]{ ((ServerBlockChangePacket) packet).getRecord() }
|
||||
: ((ServerMultiBlockChangePacket) packet).getRecords()) {
|
||||
Position pos = record.getPosition();
|
||||
Chunk chunk = activeChunks.get(coordToLong(pos.getX() / 16, pos.getZ() / 16));
|
||||
if (chunk != null) {
|
||||
BlockStorage blockStorage = chunk.currentBlockState[pos.getY() / 16];
|
||||
int x = Math.floorMod(pos.getX(), 16), y = Math.floorMod(pos.getY(), 16), z = Math.floorMod(pos.getZ(), 16);
|
||||
BlockState prevState = blockStorage.get(x, y, z);
|
||||
BlockState newState = record.getBlock();
|
||||
blockStorage.set(x, y, z, newState);
|
||||
chunk.blocks.put(time, new BlockChange(pos, prevState, newState));
|
||||
}
|
||||
}
|
||||
} else if (packet instanceof ServerPlayerListEntryPacket) {
|
||||
ServerPlayerListEntryPacket p = (ServerPlayerListEntryPacket) packet;
|
||||
if (p.getAction() == PlayerListEntryAction.ADD_PLAYER) {
|
||||
for (PlayerListEntry entry : p.getEntries()) {
|
||||
playerListEntries.put(entry.getProfile().getId(), entry);
|
||||
}
|
||||
}
|
||||
} else if (packet instanceof ServerRespawnPacket) {
|
||||
// FIXME
|
||||
}
|
||||
if (entityId != null) {
|
||||
Entity entity = activeEntities.get(entityId);
|
||||
if (entity != null) {
|
||||
Location current = entity.locations.isEmpty() ? null : entity.locations.lastEntry().getValue();
|
||||
Location updated = PacketUtils.updateLocation(current, packet);
|
||||
if (updated != null) {
|
||||
entity.locations.put(time, updated);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendPacketsTill(int replayTime) {
|
||||
ensureInitialized(() -> {
|
||||
if (replayTime > currentTimeStamp) {
|
||||
activeThings.removeIf(thing -> {
|
||||
if (thing.despawnTime < replayTime) {
|
||||
thing.despawnPackets.forEach(ctx::fireChannelRead);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
thingSpawnsT.subMap(currentTimeStamp, false, replayTime, true).values()
|
||||
.forEach(things -> things.forEach(thing -> {
|
||||
if (thing.despawnTime > replayTime) {
|
||||
thing.spawnPackets.forEach(ctx::fireChannelRead);
|
||||
activeThings.add(thing);
|
||||
}
|
||||
}));
|
||||
activeThings.forEach(thing -> thing.play(currentTimeStamp, replayTime, ctx::fireChannelRead));
|
||||
} else {
|
||||
activeThings.removeIf(thing -> {
|
||||
if (thing.spawnTime > replayTime) {
|
||||
thing.despawnPackets.forEach(ctx::fireChannelRead);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
thingDespawnsT.subMap(replayTime, false, currentTimeStamp, true).values()
|
||||
.forEach(things -> things.forEach(thing -> {
|
||||
if (thing.spawnTime <= replayTime) {
|
||||
thing.spawnPackets.forEach(ctx::fireChannelRead);
|
||||
activeThings.add(thing);
|
||||
}
|
||||
}));
|
||||
activeThings.forEach(thing -> thing.rewind(currentTimeStamp, replayTime, ctx::fireChannelRead));
|
||||
}
|
||||
currentTimeStamp = replayTime;
|
||||
});
|
||||
}
|
||||
|
||||
private static final ByteBuf byteBuf = Unpooled.buffer();
|
||||
private static final ByteBufOutputStream byteBufOut = new ByteBufOutputStream(byteBuf);
|
||||
private static final PacketBuffer packetBuf = new PacketBuffer(byteBuf);
|
||||
private static final ReplayOutputStream encoder = new ReplayOutputStream(new ReplayStudio(), byteBufOut);
|
||||
|
||||
private static Packet<?> toMC(com.github.steveice10.packetlib.packet.Packet packet) {
|
||||
// We need to re-encode MCProtocolLib packets, so we can then decode them as NMS packets
|
||||
// The main reason we aren't reading them as NMS packets is that we want ReplayStudio to be able
|
||||
// to apply ViaVersion (and potentially other magic) to it.
|
||||
synchronized (encoder) {
|
||||
int readerIndex = byteBuf.readerIndex(); // Mark the current reader and writer index (should be at start)
|
||||
int writerIndex = byteBuf.writerIndex();
|
||||
try {
|
||||
encoder.write(0, packet); // Re-encode packet, data will end up in byteBuf
|
||||
encoder.flush();
|
||||
|
||||
byteBuf.skipBytes(8); // Skip packet length & timestamp
|
||||
|
||||
int packetId = packetBuf.readVarInt();
|
||||
Packet<?> mcPacket = EnumConnectionState.PLAY.getPacket(EnumPacketDirection.CLIENTBOUND, packetId);
|
||||
mcPacket.readPacketData(packetBuf);
|
||||
return mcPacket;
|
||||
} catch (Exception e) {
|
||||
Throwables.throwIfUnchecked(e);
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
byteBuf.readerIndex(readerIndex); // Reset reader & writer index for next use
|
||||
byteBuf.writerIndex(writerIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static long coordToLong(int x, int z) {
|
||||
return (long)x << 32 | (long)z & 0xFFFFFFFFL;
|
||||
}
|
||||
|
||||
private static abstract class TrackedThing {
|
||||
List<Packet<?>> spawnPackets;
|
||||
List<Packet<?>> despawnPackets;
|
||||
int spawnTime;
|
||||
int despawnTime = Integer.MAX_VALUE;
|
||||
|
||||
public abstract void play(int currentTimeStamp, int replayTime, Consumer<Packet<?>> send);
|
||||
public abstract void rewind(int currentTimeStamp, int replayTime, Consumer<Packet<?>> send);
|
||||
}
|
||||
|
||||
private static class Entity extends TrackedThing {
|
||||
private int id;
|
||||
private NavigableMap<Integer, Location> locations = new TreeMap<>();
|
||||
|
||||
private Entity(int entityId, List<Packet<?>> spawnPackets) {
|
||||
this.id = entityId;
|
||||
this.spawnPackets = spawnPackets;
|
||||
this.despawnPackets = Collections.singletonList(toMC(new ServerEntityDestroyPacket(entityId)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void play(int currentTimeStamp, int replayTime, Consumer<Packet<?>> send) {
|
||||
Map.Entry<Integer, Location> lastUpdate = locations.floorEntry(replayTime);
|
||||
if (lastUpdate != null && lastUpdate.getKey() > currentTimeStamp) {
|
||||
Location l = lastUpdate.getValue();
|
||||
send.accept(toMC(new ServerEntityTeleportPacket(id, l.getX(), l.getY(), l.getZ(), l.getYaw(), l.getPitch(), false)));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rewind(int currentTimeStamp, int replayTime, Consumer<Packet<?>> send) {
|
||||
Map.Entry<Integer, Location> lastUpdate = locations.floorEntry(replayTime);
|
||||
if (lastUpdate != null && !lastUpdate.getKey().equals(locations.floorKey(currentTimeStamp))) {
|
||||
Location l = lastUpdate.getValue();
|
||||
send.accept(toMC(new ServerEntityTeleportPacket(id, l.getX(), l.getY(), l.getZ(), l.getYaw(), l.getPitch(), false)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class Chunk extends TrackedThing {
|
||||
private TreeMap<Integer, Collection<BlockChange>> blocksT = new TreeMap<>();
|
||||
private ListMultimap<Integer, BlockChange> blocks = Multimaps.newListMultimap(blocksT, LinkedList::new); // LinkedList to allow .descendingIterator
|
||||
private BlockStorage[] currentBlockState = new BlockStorage[16];
|
||||
|
||||
private Chunk(Column column) {
|
||||
this.spawnPackets = Collections.singletonList(toMC(new ServerChunkDataPacket(column)));
|
||||
this.despawnPackets = Collections.singletonList(toMC(new ServerUnloadChunkPacket(column.getX(), column.getZ())));
|
||||
com.github.steveice10.mc.protocol.data.game.chunk.Chunk[] chunks = column.getChunks();
|
||||
for (int i = 0; i < currentBlockState.length; i++) {
|
||||
currentBlockState[i] = chunks[i] == null ? new BlockStorage() : chunks[i].getBlocks();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void play(int currentTimeStamp, int replayTime, Consumer<Packet<?>> send) {
|
||||
blocksT.subMap(currentTimeStamp, false, replayTime, true).values()
|
||||
.forEach(updates -> updates.forEach(update -> {
|
||||
send.accept(toMC(new ServerBlockChangePacket(new BlockChangeRecord(update.pos, update.to))));
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rewind(int currentTimeStamp, int replayTime, Consumer<Packet<?>> send) {
|
||||
if (currentTimeStamp >= despawnTime) {
|
||||
play(spawnTime, replayTime, send);
|
||||
return;
|
||||
}
|
||||
blocksT.subMap(replayTime, false, currentTimeStamp, true).descendingMap().values()
|
||||
.forEach(updates ->
|
||||
((LinkedList<BlockChange>) updates).descendingIterator().forEachRemaining(update ->
|
||||
send.accept(toMC(new ServerBlockChangePacket(new BlockChangeRecord(update.pos, update.from))))));
|
||||
}
|
||||
}
|
||||
|
||||
private static class BlockChange {
|
||||
private Position pos;
|
||||
private BlockState from;
|
||||
private BlockState to;
|
||||
|
||||
private BlockChange(Position pos, BlockState from, BlockState to) {
|
||||
this.pos = pos;
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,6 @@ import java.util.*;
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import net.minecraft.client.network.NetHandlerPlayClient;
|
||||
import net.minecraft.network.EnumPacketDirection;
|
||||
import net.minecraftforge.common.MinecraftForge;
|
||||
import net.minecraftforge.fml.client.FMLClientHandler;
|
||||
import net.minecraftforge.fml.common.network.handshake.NetworkDispatcher;
|
||||
|
||||
@@ -67,7 +66,9 @@ public class ReplayHandler {
|
||||
/**
|
||||
* Decodes and sends packets into channel.
|
||||
*/
|
||||
private final ReplaySender replaySender;
|
||||
private final FullReplaySender fullReplaySender;
|
||||
private final QuickReplaySender quickReplaySender;
|
||||
private boolean quickMode = false;
|
||||
|
||||
/**
|
||||
* Currently active replay restrictions.
|
||||
@@ -85,6 +86,8 @@ public class ReplayHandler {
|
||||
|
||||
private EmbeddedChannel channel;
|
||||
|
||||
private int replayDuration;
|
||||
|
||||
/**
|
||||
* The position at which the camera should be located after the next jump.
|
||||
*/
|
||||
@@ -96,11 +99,14 @@ public class ReplayHandler {
|
||||
Preconditions.checkState(mc.isCallingFromMinecraftThread(), "Must be called from Minecraft thread.");
|
||||
this.replayFile = replayFile;
|
||||
|
||||
replayDuration = replayFile.getMetaData().getDuration();
|
||||
|
||||
FML_BUS.post(new ReplayOpenEvent.Pre(this));
|
||||
|
||||
markers = new ArrayList<>(replayFile.getMarkers().or(Collections.emptySet()));
|
||||
|
||||
replaySender = new ReplaySender(this, replayFile, false);
|
||||
fullReplaySender = new FullReplaySender(this, replayFile, false);
|
||||
quickReplaySender = new QuickReplaySender(ReplayModReplay.instance, replayFile);
|
||||
|
||||
setup();
|
||||
|
||||
@@ -109,7 +115,7 @@ public class ReplayHandler {
|
||||
|
||||
FML_BUS.post(new ReplayOpenEvent.Post(this));
|
||||
|
||||
replaySender.setAsyncMode(asyncMode);
|
||||
fullReplaySender.setAsyncMode(asyncMode);
|
||||
}
|
||||
|
||||
void restartedReplay() {
|
||||
@@ -131,7 +137,8 @@ public class ReplayHandler {
|
||||
|
||||
FML_BUS.post(new ReplayCloseEvent.Pre(this));
|
||||
|
||||
replaySender.terminateReplay();
|
||||
fullReplaySender.terminateReplay();
|
||||
quickReplaySender.unregister();
|
||||
|
||||
replayFile.save();
|
||||
replayFile.close();
|
||||
@@ -185,7 +192,8 @@ public class ReplayHandler {
|
||||
NetworkDispatcher networkDispatcher = new NetworkDispatcher(networkManager);
|
||||
channel.attr(NetworkDispatcher.FML_DISPATCHER).set(networkDispatcher);
|
||||
|
||||
channel.pipeline().addFirst("ReplayModReplay_replaySender", replaySender);
|
||||
channel.pipeline().addFirst("ReplayModReplay_replaySender", fullReplaySender);
|
||||
channel.pipeline().addFirst("ReplayModReplay_quickReplaySender", quickReplaySender);
|
||||
channel.pipeline().addLast("packet_handler", networkManager);
|
||||
channel.pipeline().fireChannelActive();
|
||||
networkDispatcher.clientToServerHandshake();
|
||||
@@ -194,7 +202,7 @@ public class ReplayHandler {
|
||||
//$$ NetworkDispatcher networkDispatcher = new NetworkDispatcher(networkManager);
|
||||
//$$ channel.attr(NetworkDispatcher.FML_DISPATCHER).set(networkDispatcher);
|
||||
//$$
|
||||
//$$ channel.pipeline().addFirst("ReplayModReplay_replaySender", replaySender);
|
||||
//$$ channel.pipeline().addFirst("ReplayModReplay_replaySender", fullReplaySender);
|
||||
//$$ channel.pipeline().addAfter("ReplayModReplay_replaySender", "fml:packet_handler", networkDispatcher);
|
||||
//$$ channel.pipeline().fireChannelActive();
|
||||
//#endif
|
||||
@@ -225,7 +233,7 @@ public class ReplayHandler {
|
||||
//$$ ChannelOutboundHandlerAdapter dummyHandler = new ChannelOutboundHandlerAdapter();
|
||||
//$$ channel = new EmbeddedChannel(dummyHandler);
|
||||
//$$ channel.pipeline().remove(dummyHandler);
|
||||
//$$ channel.pipeline().addFirst("ReplayModReplay_replaySender", replaySender);
|
||||
//$$ channel.pipeline().addFirst("ReplayModReplay_replaySender", fullReplaySender);
|
||||
//$$ channel.pipeline().addAfter("ReplayModReplay_replaySender", "packet_handler", networkManager);
|
||||
//$$ channel.pipeline().fireChannelActive();
|
||||
//$$
|
||||
@@ -247,13 +255,40 @@ public class ReplayHandler {
|
||||
}
|
||||
|
||||
public ReplaySender getReplaySender() {
|
||||
return replaySender;
|
||||
return quickMode ? quickReplaySender : fullReplaySender;
|
||||
}
|
||||
|
||||
public GuiReplayOverlay getOverlay() {
|
||||
return overlay;
|
||||
}
|
||||
|
||||
public void setQuickMode(boolean quickMode) {
|
||||
if (quickMode == this.quickMode) return;
|
||||
if (quickMode && !fullReplaySender.isAsyncMode()) return; // Cannot activate quick mode when already in sync mode
|
||||
this.quickMode = quickMode;
|
||||
if (quickMode) {
|
||||
fullReplaySender.setSyncModeAndWait();
|
||||
quickReplaySender.register();
|
||||
quickReplaySender.restart();
|
||||
quickReplaySender.sendPacketsTill(fullReplaySender.currentTimeStamp());
|
||||
quickReplaySender.setAsyncMode(true);
|
||||
} else {
|
||||
quickReplaySender.setSyncModeAndWait();
|
||||
quickReplaySender.unregister();
|
||||
fullReplaySender.sendPacketsTill(0);
|
||||
fullReplaySender.sendPacketsTill(quickReplaySender.currentTimeStamp());
|
||||
fullReplaySender.setAsyncMode(true);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isQuickMode() {
|
||||
return quickMode;
|
||||
}
|
||||
|
||||
public int getReplayDuration() {
|
||||
return replayDuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether camera movement by user inputs and/or server packets should be suppressed.
|
||||
* @return {@code true} if these kinds of movement should be suppressed
|
||||
@@ -363,6 +398,12 @@ public class ReplayHandler {
|
||||
}
|
||||
|
||||
public void doJump(int targetTime, boolean retainCameraPosition) {
|
||||
if (getReplaySender() == quickReplaySender) {
|
||||
quickReplaySender.sendPacketsTill(targetTime);
|
||||
return;
|
||||
}
|
||||
FullReplaySender replaySender = fullReplaySender;
|
||||
|
||||
if (replaySender.isHurrying()) {
|
||||
return; // When hurrying, no Timeline jumping etc. is possible
|
||||
}
|
||||
|
||||
@@ -142,6 +142,12 @@ public class ReplayModReplay {
|
||||
}
|
||||
});
|
||||
|
||||
core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.quickmode", Keyboard.KEY_Q, () -> {
|
||||
if (replayHandler != null) {
|
||||
replayHandler.setQuickMode(!replayHandler.isQuickMode());
|
||||
}
|
||||
});
|
||||
|
||||
core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.rollclockwise", Keyboard.KEY_L, () -> {
|
||||
// Noop, actual handling logic in CameraEntity#update
|
||||
});
|
||||
|
||||
1147
src/main/java/com/replaymod/replay/ReplaySender.java
Executable file → Normal file
1147
src/main/java/com/replaymod/replay/ReplaySender.java
Executable file → Normal file
File diff suppressed because it is too large
Load Diff
@@ -128,7 +128,7 @@ public class GuiReplayOverlay extends AbstractGuiOverlay<GuiReplayOverlay> {
|
||||
public void run(int time) {
|
||||
replayHandler.doJump(time, true);
|
||||
}
|
||||
}).setLength(replayHandler.getReplaySender().replayLength());
|
||||
}).setLength(replayHandler.getReplayDuration());
|
||||
}
|
||||
|
||||
public double getSpeedSliderValue() {
|
||||
|
||||
Reference in New Issue
Block a user