Files
ReplayModCinematic/src/main/java/com/replaymod/replay/QuickReplaySender.java
2020-08-09 13:55:00 +02:00

319 lines
10 KiB
Java

//#if MC>=10904
package com.replaymod.replay;
import com.github.steveice10.packetlib.io.NetInput;
import com.github.steveice10.packetlib.tcp.io.ByteBufNetInput;
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.mixin.MinecraftAccessor;
import com.replaymod.core.mixin.TimerAccessor;
import com.replaymod.replaystudio.replay.ReplayFile;
import com.replaymod.replaystudio.util.RandomAccessReplay;
import de.johni0702.minecraft.gui.utils.EventRegistrations;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import net.minecraft.client.MinecraftClient;
import net.minecraft.network.packet.s2c.play.PlayerPositionLookS2CPacket;
import net.minecraft.network.packet.s2c.play.PlayerRespawnS2CPacket;
import net.minecraft.network.NetworkState;
import net.minecraft.network.NetworkSide;
import net.minecraft.network.Packet;
import net.minecraft.util.PacketByteBuf;
//#if FABRIC>=1
import de.johni0702.minecraft.gui.versions.callbacks.PreTickCallback;
//#else
//$$ import net.minecraftforge.eventbus.api.SubscribeEvent;
//$$ import net.minecraftforge.event.TickEvent;
//$$
//$$ import static com.replaymod.core.versions.MCVer.FML_BUS;
//#endif
import javax.annotation.Nullable;
import java.io.IOException;
import java.util.Collections;
import java.util.function.Consumer;
//#if MC>=11602
//$$ import net.minecraft.util.registry.DynamicRegistryManager;
//$$ import net.minecraft.util.registry.Registry;
//#endif
//#if MC>=11600
//$$ import net.minecraft.world.World;
//#else
import net.minecraft.world.level.LevelGeneratorType;
//#endif
//#if MC>=11400
import net.minecraft.world.dimension.DimensionType;
//#else
//$$ import net.minecraft.world.EnumDifficulty;
//#endif
//#if MC>=11200
import com.replaymod.core.utils.WrappedTimer;
//#endif
//#if MC>=11002
import net.minecraft.world.GameMode;
//#else
//$$ import net.minecraft.world.WorldSettings.GameType;
//#endif
import static com.replaymod.core.versions.MCVer.getMinecraft;
import static com.replaymod.core.versions.MCVer.getPacketTypeRegistry;
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 MinecraftClient mc = getMinecraft();
private final ReplayModReplay mod;
private final RandomAccessReplay<Packet<?>> replay;
private final EventHandler eventHandler = new EventHandler();
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 com.github.steveice10.netty.buffer.ByteBuf buf;
private NetInput bufInput;
public QuickReplaySender(ReplayModReplay mod, ReplayFile replayFile) {
this.mod = mod;
this.replay = new RandomAccessReplay<Packet<?>>(replayFile, getPacketTypeRegistry(false)) {
private byte[] buf = new byte[0];
@Override
protected Packet<?> decode(com.github.steveice10.netty.buffer.ByteBuf byteBuf) throws IOException {
int packetId = new ByteBufNetInput(byteBuf).readVarInt();
//#if MC>=11500
Packet<?> mcPacket = NetworkState.PLAY.getPacketHandler(NetworkSide.CLIENTBOUND, packetId);
//#else
//$$ Packet<?> mcPacket;
//$$ try {
//$$ mcPacket = NetworkState.PLAY.getPacketHandler(NetworkSide.CLIENTBOUND, packetId);
//$$ } catch (IllegalAccessException | InstantiationException e) {
//$$ throw new IOException(e);
//$$ }
//#endif
if (mcPacket != null) {
int size = byteBuf.readableBytes();
if (buf.length < size) {
buf = new byte[size];
}
byteBuf.readBytes(buf, 0, size);
ByteBuf wrappedBuf = Unpooled.wrappedBuffer(buf);
wrappedBuf.writerIndex(size);
mcPacket.read(new PacketByteBuf(wrappedBuf));
}
return mcPacket;
}
@Override
protected void dispatch(Packet<?> packet) {
ctx.fireChannelRead(packet);
}
};
}
public void register() {
eventHandler.register();
}
public void unregister() {
eventHandler.unregister();
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) {
this.ctx = ctx;
}
public ListenableFuture<Void> getInitializationPromise() {
return initPromise;
}
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();
replay.load(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;
}
private void ensureInitialized(Runnable body) {
if (initPromise == null) {
LOGGER.warn("QuickReplaySender used without prior initialization!", new Throwable());
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() {
replay.reset();
ctx.fireChannelRead(new PlayerRespawnS2CPacket(
//#if MC>=11600
//#if MC>=11602
//$$ DimensionType.addRegistryDefaults(new DynamicRegistryManager.Impl()).get(Registry.DIMENSION_TYPE_KEY).get(DimensionType.OVERWORLD_REGISTRY_KEY),
//#else
//$$ DimensionType.OVERWORLD_REGISTRY_KEY,
//#endif
//$$ World.OVERWORLD,
//$$ 0,
//$$ GameMode.SPECTATOR,
//$$ GameMode.SPECTATOR,
//$$ false,
//$$ false,
//$$ false
//#else
//#if MC>=11400
DimensionType.OVERWORLD,
//#else
//$$ 0,
//#endif
//#if MC>=11500
0,
//#endif
//#if MC<11400
//$$ EnumDifficulty.NORMAL,
//#endif
LevelGeneratorType.DEFAULT,
GameMode.SPECTATOR
//#endif
));
ctx.fireChannelRead(new PlayerPositionLookS2CPacket(0, 0, 0, 0, 0, Collections.emptySet(), 0));
}
@Override
public int currentTimeStamp() {
return currentTimeStamp;
}
@Override
public void setReplaySpeed(double factor) {
if (factor != 0) {
if (paused() && asyncMode) {
lastAsyncUpdateTime = System.currentTimeMillis();
}
this.replaySpeed = factor;
}
TimerAccessor timer = (TimerAccessor) ((MinecraftAccessor) mc).getTimer();
//#if MC>=11200
timer.setTickLength(WrappedTimer.DEFAULT_MS_PER_TICK / (float) factor);
//#else
//$$ timer.setTimerSpeed((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);
}
private class EventHandler extends EventRegistrations {
//#if FABRIC>=1
{ on(PreTickCallback.EVENT, this::onTick); }
private void onTick() {
//#else
//$$ @SubscribeEvent
//$$ public void onTick(TickEvent.ClientTickEvent event) {
//$$ if (event.phase != TickEvent.Phase.START) return;
//#endif
if (!asyncMode || paused()) return;
long now = System.currentTimeMillis();
long realTimePassed = now - lastAsyncUpdateTime;
lastAsyncUpdateTime = now;
int replayTimePassed = (int) (realTimePassed * replaySpeed);
sendPacketsTill(currentTimeStamp + replayTimePassed);
}
}
@Override
public void sendPacketsTill(int replayTime) {
ensureInitialized(() -> {
try {
replay.seek(replayTime);
} catch (IOException e) {
e.printStackTrace();
}
currentTimeStamp = replayTime;
});
}
}
//#endif