Skip to content

Commit 3b47671

Browse files
committed
Better working version
1 parent 086194e commit 3b47671

File tree

7 files changed

+291
-78
lines changed

7 files changed

+291
-78
lines changed

src/main/java/me/senseiwells/replay/ducks/ServerReplay$ReplayViewable.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import org.jetbrains.annotations.Nullable;
66

77
public interface ServerReplay$ReplayViewable {
8-
void replay$setReplayViewer(ReplayViewer viewer);
8+
void replay$setReplayViewer(@Nullable ReplayViewer viewer);
99

1010
@Nullable ReplayViewer replay$getReplayViewer();
1111

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package me.senseiwells.replay.mixin.viewer;
2+
3+
import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket;
4+
import org.spongepowered.asm.mixin.Mixin;
5+
import org.spongepowered.asm.mixin.Mutable;
6+
import org.spongepowered.asm.mixin.gen.Accessor;
7+
8+
import java.util.List;
9+
10+
@Mixin(ClientboundPlayerInfoUpdatePacket.class)
11+
public interface ClientboundPlayerInfoUpdatePacketAccessor {
12+
@Mutable
13+
@Accessor("entries")
14+
void setEntries(List<ClientboundPlayerInfoUpdatePacket.Entry> entries);
15+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package me.senseiwells.replay.mixin.viewer;
2+
3+
import net.minecraft.world.entity.Entity;
4+
import org.spongepowered.asm.mixin.Mixin;
5+
import org.spongepowered.asm.mixin.gen.Invoker;
6+
7+
@Mixin(Entity.class)
8+
public interface EntityInvoker {
9+
@Invoker("unsetRemoved")
10+
void removeRemovalReason();
11+
}

src/main/kotlin/me/senseiwells/replay/commands/ReplayCommand.kt

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import com.mojang.brigadier.arguments.IntegerArgumentType
66
import com.mojang.brigadier.arguments.StringArgumentType
77
import com.mojang.brigadier.context.CommandContext
88
import com.mojang.brigadier.suggestion.SuggestionProvider
9-
import com.replaymod.replaystudio.replay.ReplayFile
109
import com.replaymod.replaystudio.replay.ZipReplayFile
1110
import com.replaymod.replaystudio.studio.ReplayStudio
1211
import me.lucko.fabric.api.permissions.v0.Permissions
@@ -18,6 +17,7 @@ import me.senseiwells.replay.config.ReplayConfig
1817
import me.senseiwells.replay.player.PlayerRecorders
1918
import me.senseiwells.replay.recorder.ReplayRecorder
2019
import me.senseiwells.replay.viewer.ReplayViewer
20+
import me.senseiwells.replay.viewer.ReplayViewerUtils.getReplayViewer
2121
import net.fabricmc.loader.api.FabricLoader
2222
import net.minecraft.commands.CommandSourceStack
2323
import net.minecraft.commands.Commands
@@ -135,7 +135,11 @@ object ReplayCommand {
135135
Commands.literal("status").executes(this::status)
136136
).then(
137137
Commands.literal("view").then(
138-
Commands.argument("path", StringArgumentType.greedyString()).executes(this::viewReplay)
138+
Commands.literal("start").then(
139+
Commands.argument("path", StringArgumentType.greedyString()).executes(this::viewReplay)
140+
)
141+
).then(
142+
Commands.literal("stop").executes(this::stopViewReplay)
139143
)
140144
)
141145
)
@@ -350,13 +354,19 @@ object ReplayCommand {
350354
ZipReplayFile(ReplayStudio(), location.toFile()),
351355
player.connection
352356
)
353-
viewer.view()
357+
viewer.start()
354358
} else {
355359
// TODO!
356360
}
357361
return 1
358362
}
359363

364+
private fun stopViewReplay(context: CommandContext<CommandSourceStack>): Int {
365+
val player = context.source.playerOrException
366+
player.connection.getReplayViewer()?.stop()
367+
return 1
368+
}
369+
360370
private fun getStatusFuture(
361371
type: String,
362372
recorders: Collection<ReplayRecorder>
Lines changed: 173 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,103 +1,202 @@
11
package me.senseiwells.replay.viewer
22

3+
import com.replaymod.replaystudio.PacketData
34
import com.replaymod.replaystudio.lib.viaversion.api.protocol.packet.State
45
import com.replaymod.replaystudio.lib.viaversion.api.protocol.version.ProtocolVersion
5-
import com.replaymod.replaystudio.protocol.Packet
66
import com.replaymod.replaystudio.protocol.PacketTypeRegistry
77
import com.replaymod.replaystudio.replay.ReplayFile
8-
import io.netty.buffer.ByteBuf
9-
import io.netty.buffer.Unpooled
10-
import me.senseiwells.replay.ducks.`ServerReplay$ReplayViewable`
11-
import net.fabricmc.fabric.impl.networking.payload.RetainedPayload
8+
import it.unimi.dsi.fastutil.ints.IntArrayList
9+
import it.unimi.dsi.fastutil.ints.IntList
10+
import it.unimi.dsi.fastutil.ints.IntOpenHashSet
11+
import it.unimi.dsi.fastutil.longs.LongOpenHashSet
12+
import kotlinx.coroutines.*
13+
import me.senseiwells.replay.mixin.viewer.EntityInvoker
14+
import me.senseiwells.replay.viewer.ReplayViewerUtils.getClientboundPlayPacketType
15+
import me.senseiwells.replay.viewer.ReplayViewerUtils.sendReplayPacket
16+
import me.senseiwells.replay.viewer.ReplayViewerUtils.setReplayViewer
17+
import me.senseiwells.replay.viewer.ReplayViewerUtils.toClientboundPlayPacket
1218
import net.minecraft.SharedConstants
13-
import net.minecraft.network.ConnectionProtocol
14-
import net.minecraft.network.FriendlyByteBuf
15-
import net.minecraft.network.protocol.PacketFlow
16-
import net.minecraft.network.protocol.common.ClientboundCustomPayloadPacket
17-
import net.minecraft.network.protocol.game.ClientboundForgetLevelChunkPacket
18-
import net.minecraft.network.protocol.game.ClientboundLoginPacket
19-
import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket
19+
import net.minecraft.core.UUIDUtil
20+
import net.minecraft.network.chat.Component
21+
import net.minecraft.network.protocol.Packet
22+
import net.minecraft.network.protocol.game.*
23+
import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket.Action
24+
import net.minecraft.server.level.ServerPlayer
2025
import net.minecraft.server.network.ServerGamePacketListenerImpl
21-
import net.minecraft.network.protocol.Packet as MinecraftPacket
26+
import net.minecraft.world.entity.Entity
27+
import net.minecraft.world.level.ChunkPos
28+
import net.minecraft.world.phys.Vec3
29+
import java.util.*
30+
import kotlin.collections.ArrayList
31+
import kotlin.collections.HashMap
2232

2333
class ReplayViewer(
2434
val replay: ReplayFile,
2535
val connection: ServerGamePacketListenerImpl
2636
) {
27-
fun view() {
28-
(this.connection as `ServerReplay$ReplayViewable`).`replay$setReplayViewer`(this)
37+
private var started = false
2938

30-
this.connection.`replay$sendReplayViewerPacket`(
31-
ClientboundPlayerInfoUpdatePacket(
32-
ClientboundPlayerInfoUpdatePacket.Action.UPDATE_GAME_MODE,
33-
this.connection.player
34-
)
35-
)
36-
this.connection.player.chunkTrackingView.forEach {
37-
this.connection.`replay$sendReplayViewerPacket`(ClientboundForgetLevelChunkPacket(it))
39+
private val coroutineScope by lazy {
40+
CoroutineScope(Dispatchers.Default + Job())
41+
}
42+
private val chunks = Collections.synchronizedCollection(LongOpenHashSet())
43+
private val entities = Collections.synchronizedCollection(IntOpenHashSet())
44+
45+
private val player: ServerPlayer
46+
get() = this.connection.player
47+
48+
fun start() {
49+
if (this.started) {
50+
return
3851
}
52+
this.started = true
53+
this.setForReplayView()
3954

40-
// TODO: Wtf is this???
41-
// This is so fucking jank please fix this :)
42-
Thread {
55+
this.coroutineScope.launch {
4356
val version = ProtocolVersion.getProtocol(SharedConstants.getProtocolVersion())
4457
// TODO: Send any configuration data (e.g. resource packs, tags, etc. to the client)
45-
val data = this.replay.getPacketData(PacketTypeRegistry.get(version, State.PLAY))
46-
47-
var pastLogin = false
48-
var previousTime = 0L
49-
var timed = data.readPacket()
50-
while (timed != null) {
51-
// TODO: Ew, please make this nicer
52-
val type = ConnectionProtocol.PLAY.getPacketsByIds(PacketFlow.CLIENTBOUND).get(timed.packet.id)
53-
if (type == ClientboundLoginPacket::class.java) {
54-
pastLogin = true
55-
timed.release()
56-
timed = data.readPacket()
57-
continue
58-
} else if (!pastLogin) {
59-
timed.release()
60-
timed = data.readPacket()
61-
continue
58+
val stream = replay.getPacketData(PacketTypeRegistry.get(version, State.PLAY))
59+
60+
var loggedIn = false
61+
var lastTime = 0L
62+
var data: PacketData? = stream.readPacket()
63+
while (data != null) {
64+
// We don't care about all the packets before the player logs in.
65+
if (!loggedIn) {
66+
val type = data.packet.getClientboundPlayPacketType()
67+
if (type != ClientboundLoginPacket::class.java) {
68+
data.release()
69+
data = stream.readPacket()
70+
continue
71+
}
72+
loggedIn = true
6273
}
63-
// TODO: Use co-routines for non-blocking delay
64-
Thread.sleep(timed.time - previousTime)
65-
this.connection.`replay$sendReplayViewerPacket`(toMinecraftPacket(timed.packet))
6674

67-
previousTime = timed.time
68-
timed.release()
69-
timed = data.readPacket()
75+
delay(data.time - lastTime)
76+
77+
val packet = data.packet.toClientboundPlayPacket()
78+
send(modifyViewingPacket(packet))
79+
80+
lastTime = data.time
81+
data.release()
82+
data = stream.readPacket()
7083
}
71-
}.start()
84+
}
7285
}
7386

74-
private companion object {
75-
@Suppress("UnstableApiUsage")
76-
fun toMinecraftPacket(packet: Packet): MinecraftPacket<*> {
77-
val codec = ConnectionProtocol.PLAY.codec(PacketFlow.CLIENTBOUND)
78-
val decoded = codec.createPacket(packet.id, toFriendlyByteBuf(packet.buf))
79-
?: throw IllegalStateException("Failed to create packet with id ${packet.id}")
80-
81-
if (decoded is ClientboundCustomPayloadPacket) {
82-
val payload = decoded.payload
83-
if (payload is RetainedPayload) {
84-
return ClientboundCustomPayloadPacket(payload.resolve(null))
85-
}
86-
}
87-
return decoded
87+
fun stop() {
88+
this.coroutineScope.cancel()
89+
this.connection.setReplayViewer(null)
90+
91+
this.connection.send(ClientboundRemoveEntitiesPacket(IntArrayList(this.entities)))
92+
93+
for (chunk in this.chunks) {
94+
this.connection.send(ClientboundForgetLevelChunkPacket(ChunkPos(chunk)))
8895
}
96+
val player = this.player
97+
val level = player.serverLevel()
98+
level.addNewPlayer(player)
99+
this.connection.send(ClientboundRespawnPacket(player.createCommonSpawnInfo(level), 3.toByte()))
100+
(player as EntityInvoker).removeRemovalReason()
101+
this.connection.teleport(player.x, player.y, player.z, player.yRot, player.xRot)
102+
player.server.playerList.sendLevelInfo(player, level)
103+
}
89104

90-
@Suppress("USELESS_IS_CHECK")
91-
fun toFriendlyByteBuf(buf: com.github.steveice10.netty.buffer.ByteBuf): FriendlyByteBuf {
92-
// When we compile we map steveice10.netty -> io.netty
93-
// We just need this check for dev environment
94-
if (buf is ByteBuf) {
95-
return FriendlyByteBuf(buf)
96-
}
105+
private fun setForReplayView() {
106+
this.connection.setReplayViewer(this)
107+
this.player.serverLevel().removePlayerImmediately(this.player, Entity.RemovalReason.CHANGED_DIMENSION);
108+
109+
this.send(
110+
ClientboundPlayerInfoUpdatePacket(
111+
Action.UPDATE_GAME_MODE,
112+
this.player
113+
)
114+
)
115+
this.player.chunkTrackingView.forEach {
116+
this.send(ClientboundForgetLevelChunkPacket(it))
117+
}
118+
}
119+
120+
private fun modifyViewingPacket(packet: Packet<*>): Packet<*> {
121+
if (packet is ClientboundLevelChunkWithLightPacket) {
122+
this.chunks.add(ChunkPos.asLong(packet.x, packet.z))
123+
}
124+
if (packet is ClientboundForgetLevelChunkPacket) {
125+
this.chunks.remove(packet.pos.toLong())
126+
}
127+
if (packet is ClientboundAddEntityPacket) {
128+
this.entities.add(packet.id)
129+
}
130+
if (packet is ClientboundRemoveEntitiesPacket) {
131+
this.entities.removeAll(packet.entityIds)
132+
}
97133

98-
val array = ByteArray(buf.readableBytes())
99-
buf.readBytes(array)
100-
return FriendlyByteBuf(Unpooled.wrappedBuffer(array))
134+
if (packet is ClientboundLoginPacket) {
135+
return ClientboundLoginPacket(
136+
VIEWER_ID,
137+
packet.hardcore,
138+
packet.levels,
139+
packet.maxPlayers,
140+
packet.chunkRadius,
141+
packet.simulationDistance,
142+
packet.reducedDebugInfo,
143+
packet.showDeathScreen,
144+
packet.doLimitedCrafting,
145+
packet.commonPlayerSpawnInfo
146+
)
147+
}
148+
if (packet is ClientboundPlayerInfoUpdatePacket) {
149+
val index = packet.entries().indexOfFirst { it.profileId == this.player.uuid }
150+
if (index >= 0) {
151+
val copy = ArrayList(packet.entries())
152+
val previous = copy[index]
153+
copy[index] = ClientboundPlayerInfoUpdatePacket.Entry(
154+
VIEWER_UUID,
155+
previous.profile,
156+
false,
157+
previous.latency,
158+
previous.gameMode,
159+
previous.displayName,
160+
previous.chatSession
161+
)
162+
return ReplayViewerUtils.createClientboundPlayerInfoUpdatePacket(packet.actions(), copy)
163+
}
164+
}
165+
if (packet is ClientboundAddEntityPacket && packet.uuid == this.player.uuid) {
166+
return ClientboundAddEntityPacket(
167+
packet.id,
168+
VIEWER_UUID,
169+
packet.x,
170+
packet.y,
171+
packet.z,
172+
packet.yRot,
173+
packet.xRot,
174+
packet.type,
175+
packet.data,
176+
Vec3(packet.xa, packet.ya, packet.za),
177+
packet.yHeadRot.toDouble()
178+
)
179+
}
180+
if (packet is ClientboundPlayerChatPacket) {
181+
// We don't want to deal with chat validation...
182+
val message = packet.unsignedContent ?: Component.literal(packet.body.content)
183+
val type = packet.chatType.resolve(this.player.server.registryAccess())
184+
val decorated = if (type.isPresent) {
185+
type.get().decorate(message)
186+
} else {
187+
Component.literal("<").append(packet.chatType.name).append("> ").append(message)
188+
}
189+
return ClientboundSystemChatPacket(decorated, false)
101190
}
191+
return packet
192+
}
193+
194+
private fun send(packet: Packet<*>) {
195+
this.connection.sendReplayPacket(packet)
196+
}
197+
198+
private companion object {
199+
const val VIEWER_ID = Int.MAX_VALUE - 10
200+
val VIEWER_UUID: UUID = UUIDUtil.createOfflinePlayerUUID("-ViewingProfile-")
102201
}
103202
}

0 commit comments

Comments
 (0)