Skip to content

Commit

Permalink
Better working version
Browse files Browse the repository at this point in the history
  • Loading branch information
senseiwells committed Mar 30, 2024
1 parent 086194e commit 3b47671
Show file tree
Hide file tree
Showing 7 changed files with 291 additions and 78 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import org.jetbrains.annotations.Nullable;

public interface ServerReplay$ReplayViewable {
void replay$setReplayViewer(ReplayViewer viewer);
void replay$setReplayViewer(@Nullable ReplayViewer viewer);

@Nullable ReplayViewer replay$getReplayViewer();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package me.senseiwells.replay.mixin.viewer;

import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Mutable;
import org.spongepowered.asm.mixin.gen.Accessor;

import java.util.List;

@Mixin(ClientboundPlayerInfoUpdatePacket.class)
public interface ClientboundPlayerInfoUpdatePacketAccessor {
@Mutable
@Accessor("entries")
void setEntries(List<ClientboundPlayerInfoUpdatePacket.Entry> entries);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package me.senseiwells.replay.mixin.viewer;

import net.minecraft.world.entity.Entity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Invoker;

@Mixin(Entity.class)
public interface EntityInvoker {
@Invoker("unsetRemoved")
void removeRemovalReason();
}
16 changes: 13 additions & 3 deletions src/main/kotlin/me/senseiwells/replay/commands/ReplayCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import com.mojang.brigadier.arguments.IntegerArgumentType
import com.mojang.brigadier.arguments.StringArgumentType
import com.mojang.brigadier.context.CommandContext
import com.mojang.brigadier.suggestion.SuggestionProvider
import com.replaymod.replaystudio.replay.ReplayFile
import com.replaymod.replaystudio.replay.ZipReplayFile
import com.replaymod.replaystudio.studio.ReplayStudio
import me.lucko.fabric.api.permissions.v0.Permissions
Expand All @@ -18,6 +17,7 @@ import me.senseiwells.replay.config.ReplayConfig
import me.senseiwells.replay.player.PlayerRecorders
import me.senseiwells.replay.recorder.ReplayRecorder
import me.senseiwells.replay.viewer.ReplayViewer
import me.senseiwells.replay.viewer.ReplayViewerUtils.getReplayViewer
import net.fabricmc.loader.api.FabricLoader
import net.minecraft.commands.CommandSourceStack
import net.minecraft.commands.Commands
Expand Down Expand Up @@ -135,7 +135,11 @@ object ReplayCommand {
Commands.literal("status").executes(this::status)
).then(
Commands.literal("view").then(
Commands.argument("path", StringArgumentType.greedyString()).executes(this::viewReplay)
Commands.literal("start").then(
Commands.argument("path", StringArgumentType.greedyString()).executes(this::viewReplay)
)
).then(
Commands.literal("stop").executes(this::stopViewReplay)
)
)
)
Expand Down Expand Up @@ -350,13 +354,19 @@ object ReplayCommand {
ZipReplayFile(ReplayStudio(), location.toFile()),
player.connection
)
viewer.view()
viewer.start()
} else {
// TODO!
}
return 1
}

private fun stopViewReplay(context: CommandContext<CommandSourceStack>): Int {
val player = context.source.playerOrException
player.connection.getReplayViewer()?.stop()
return 1
}

private fun getStatusFuture(
type: String,
recorders: Collection<ReplayRecorder>
Expand Down
247 changes: 173 additions & 74 deletions src/main/kotlin/me/senseiwells/replay/viewer/ReplayViewer.kt
Original file line number Diff line number Diff line change
@@ -1,103 +1,202 @@
package me.senseiwells.replay.viewer

import com.replaymod.replaystudio.PacketData
import com.replaymod.replaystudio.lib.viaversion.api.protocol.packet.State
import com.replaymod.replaystudio.lib.viaversion.api.protocol.version.ProtocolVersion
import com.replaymod.replaystudio.protocol.Packet
import com.replaymod.replaystudio.protocol.PacketTypeRegistry
import com.replaymod.replaystudio.replay.ReplayFile
import io.netty.buffer.ByteBuf
import io.netty.buffer.Unpooled
import me.senseiwells.replay.ducks.`ServerReplay$ReplayViewable`
import net.fabricmc.fabric.impl.networking.payload.RetainedPayload
import it.unimi.dsi.fastutil.ints.IntArrayList
import it.unimi.dsi.fastutil.ints.IntList
import it.unimi.dsi.fastutil.ints.IntOpenHashSet
import it.unimi.dsi.fastutil.longs.LongOpenHashSet
import kotlinx.coroutines.*
import me.senseiwells.replay.mixin.viewer.EntityInvoker
import me.senseiwells.replay.viewer.ReplayViewerUtils.getClientboundPlayPacketType
import me.senseiwells.replay.viewer.ReplayViewerUtils.sendReplayPacket
import me.senseiwells.replay.viewer.ReplayViewerUtils.setReplayViewer
import me.senseiwells.replay.viewer.ReplayViewerUtils.toClientboundPlayPacket
import net.minecraft.SharedConstants
import net.minecraft.network.ConnectionProtocol
import net.minecraft.network.FriendlyByteBuf
import net.minecraft.network.protocol.PacketFlow
import net.minecraft.network.protocol.common.ClientboundCustomPayloadPacket
import net.minecraft.network.protocol.game.ClientboundForgetLevelChunkPacket
import net.minecraft.network.protocol.game.ClientboundLoginPacket
import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket
import net.minecraft.core.UUIDUtil
import net.minecraft.network.chat.Component
import net.minecraft.network.protocol.Packet
import net.minecraft.network.protocol.game.*
import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket.Action
import net.minecraft.server.level.ServerPlayer
import net.minecraft.server.network.ServerGamePacketListenerImpl
import net.minecraft.network.protocol.Packet as MinecraftPacket
import net.minecraft.world.entity.Entity
import net.minecraft.world.level.ChunkPos
import net.minecraft.world.phys.Vec3
import java.util.*
import kotlin.collections.ArrayList
import kotlin.collections.HashMap

class ReplayViewer(
val replay: ReplayFile,
val connection: ServerGamePacketListenerImpl
) {
fun view() {
(this.connection as `ServerReplay$ReplayViewable`).`replay$setReplayViewer`(this)
private var started = false

this.connection.`replay$sendReplayViewerPacket`(
ClientboundPlayerInfoUpdatePacket(
ClientboundPlayerInfoUpdatePacket.Action.UPDATE_GAME_MODE,
this.connection.player
)
)
this.connection.player.chunkTrackingView.forEach {
this.connection.`replay$sendReplayViewerPacket`(ClientboundForgetLevelChunkPacket(it))
private val coroutineScope by lazy {
CoroutineScope(Dispatchers.Default + Job())
}
private val chunks = Collections.synchronizedCollection(LongOpenHashSet())
private val entities = Collections.synchronizedCollection(IntOpenHashSet())

private val player: ServerPlayer
get() = this.connection.player

fun start() {
if (this.started) {
return
}
this.started = true
this.setForReplayView()

// TODO: Wtf is this???
// This is so fucking jank please fix this :)
Thread {
this.coroutineScope.launch {
val version = ProtocolVersion.getProtocol(SharedConstants.getProtocolVersion())
// TODO: Send any configuration data (e.g. resource packs, tags, etc. to the client)
val data = this.replay.getPacketData(PacketTypeRegistry.get(version, State.PLAY))

var pastLogin = false
var previousTime = 0L
var timed = data.readPacket()
while (timed != null) {
// TODO: Ew, please make this nicer
val type = ConnectionProtocol.PLAY.getPacketsByIds(PacketFlow.CLIENTBOUND).get(timed.packet.id)
if (type == ClientboundLoginPacket::class.java) {
pastLogin = true
timed.release()
timed = data.readPacket()
continue
} else if (!pastLogin) {
timed.release()
timed = data.readPacket()
continue
val stream = replay.getPacketData(PacketTypeRegistry.get(version, State.PLAY))

var loggedIn = false
var lastTime = 0L
var data: PacketData? = stream.readPacket()
while (data != null) {
// We don't care about all the packets before the player logs in.
if (!loggedIn) {
val type = data.packet.getClientboundPlayPacketType()
if (type != ClientboundLoginPacket::class.java) {
data.release()
data = stream.readPacket()
continue
}
loggedIn = true
}
// TODO: Use co-routines for non-blocking delay
Thread.sleep(timed.time - previousTime)
this.connection.`replay$sendReplayViewerPacket`(toMinecraftPacket(timed.packet))

previousTime = timed.time
timed.release()
timed = data.readPacket()
delay(data.time - lastTime)

val packet = data.packet.toClientboundPlayPacket()
send(modifyViewingPacket(packet))

lastTime = data.time
data.release()
data = stream.readPacket()
}
}.start()
}
}

private companion object {
@Suppress("UnstableApiUsage")
fun toMinecraftPacket(packet: Packet): MinecraftPacket<*> {
val codec = ConnectionProtocol.PLAY.codec(PacketFlow.CLIENTBOUND)
val decoded = codec.createPacket(packet.id, toFriendlyByteBuf(packet.buf))
?: throw IllegalStateException("Failed to create packet with id ${packet.id}")

if (decoded is ClientboundCustomPayloadPacket) {
val payload = decoded.payload
if (payload is RetainedPayload) {
return ClientboundCustomPayloadPacket(payload.resolve(null))
}
}
return decoded
fun stop() {
this.coroutineScope.cancel()
this.connection.setReplayViewer(null)

this.connection.send(ClientboundRemoveEntitiesPacket(IntArrayList(this.entities)))

for (chunk in this.chunks) {
this.connection.send(ClientboundForgetLevelChunkPacket(ChunkPos(chunk)))
}
val player = this.player
val level = player.serverLevel()
level.addNewPlayer(player)
this.connection.send(ClientboundRespawnPacket(player.createCommonSpawnInfo(level), 3.toByte()))
(player as EntityInvoker).removeRemovalReason()
this.connection.teleport(player.x, player.y, player.z, player.yRot, player.xRot)
player.server.playerList.sendLevelInfo(player, level)
}

@Suppress("USELESS_IS_CHECK")
fun toFriendlyByteBuf(buf: com.github.steveice10.netty.buffer.ByteBuf): FriendlyByteBuf {
// When we compile we map steveice10.netty -> io.netty
// We just need this check for dev environment
if (buf is ByteBuf) {
return FriendlyByteBuf(buf)
}
private fun setForReplayView() {
this.connection.setReplayViewer(this)
this.player.serverLevel().removePlayerImmediately(this.player, Entity.RemovalReason.CHANGED_DIMENSION);

this.send(
ClientboundPlayerInfoUpdatePacket(
Action.UPDATE_GAME_MODE,
this.player
)
)
this.player.chunkTrackingView.forEach {
this.send(ClientboundForgetLevelChunkPacket(it))
}
}

private fun modifyViewingPacket(packet: Packet<*>): Packet<*> {
if (packet is ClientboundLevelChunkWithLightPacket) {
this.chunks.add(ChunkPos.asLong(packet.x, packet.z))
}
if (packet is ClientboundForgetLevelChunkPacket) {
this.chunks.remove(packet.pos.toLong())
}
if (packet is ClientboundAddEntityPacket) {
this.entities.add(packet.id)
}
if (packet is ClientboundRemoveEntitiesPacket) {
this.entities.removeAll(packet.entityIds)
}

val array = ByteArray(buf.readableBytes())
buf.readBytes(array)
return FriendlyByteBuf(Unpooled.wrappedBuffer(array))
if (packet is ClientboundLoginPacket) {
return ClientboundLoginPacket(
VIEWER_ID,
packet.hardcore,
packet.levels,
packet.maxPlayers,
packet.chunkRadius,
packet.simulationDistance,
packet.reducedDebugInfo,
packet.showDeathScreen,
packet.doLimitedCrafting,
packet.commonPlayerSpawnInfo
)
}
if (packet is ClientboundPlayerInfoUpdatePacket) {
val index = packet.entries().indexOfFirst { it.profileId == this.player.uuid }
if (index >= 0) {
val copy = ArrayList(packet.entries())
val previous = copy[index]
copy[index] = ClientboundPlayerInfoUpdatePacket.Entry(
VIEWER_UUID,
previous.profile,
false,
previous.latency,
previous.gameMode,
previous.displayName,
previous.chatSession
)
return ReplayViewerUtils.createClientboundPlayerInfoUpdatePacket(packet.actions(), copy)
}
}
if (packet is ClientboundAddEntityPacket && packet.uuid == this.player.uuid) {
return ClientboundAddEntityPacket(
packet.id,
VIEWER_UUID,
packet.x,
packet.y,
packet.z,
packet.yRot,
packet.xRot,
packet.type,
packet.data,
Vec3(packet.xa, packet.ya, packet.za),
packet.yHeadRot.toDouble()
)
}
if (packet is ClientboundPlayerChatPacket) {
// We don't want to deal with chat validation...
val message = packet.unsignedContent ?: Component.literal(packet.body.content)
val type = packet.chatType.resolve(this.player.server.registryAccess())
val decorated = if (type.isPresent) {
type.get().decorate(message)
} else {
Component.literal("<").append(packet.chatType.name).append("> ").append(message)
}
return ClientboundSystemChatPacket(decorated, false)
}
return packet
}

private fun send(packet: Packet<*>) {
this.connection.sendReplayPacket(packet)
}

private companion object {
const val VIEWER_ID = Int.MAX_VALUE - 10
val VIEWER_UUID: UUID = UUIDUtil.createOfflinePlayerUUID("-ViewingProfile-")
}
}
Loading

0 comments on commit 3b47671

Please sign in to comment.