1
1
package me.senseiwells.replay.viewer
2
2
3
+ import com.replaymod.replaystudio.PacketData
3
4
import com.replaymod.replaystudio.lib.viaversion.api.protocol.packet.State
4
5
import com.replaymod.replaystudio.lib.viaversion.api.protocol.version.ProtocolVersion
5
- import com.replaymod.replaystudio.protocol.Packet
6
6
import com.replaymod.replaystudio.protocol.PacketTypeRegistry
7
7
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
12
18
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
20
25
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
22
32
23
33
class ReplayViewer (
24
34
val replay : ReplayFile ,
25
35
val connection : ServerGamePacketListenerImpl
26
36
) {
27
- fun view () {
28
- (this .connection as `ServerReplay $ReplayViewable `).`replay$setReplayViewer`(this )
37
+ private var started = false
29
38
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
38
51
}
52
+ this .started = true
53
+ this .setForReplayView()
39
54
40
- // TODO: Wtf is this???
41
- // This is so fucking jank please fix this :)
42
- Thread {
55
+ this .coroutineScope.launch {
43
56
val version = ProtocolVersion .getProtocol(SharedConstants .getProtocolVersion())
44
57
// 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
62
73
}
63
- // TODO: Use co-routines for non-blocking delay
64
- Thread .sleep(timed.time - previousTime)
65
- this .connection.`replay$sendReplayViewerPacket`(toMinecraftPacket(timed.packet))
66
74
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()
70
83
}
71
- }.start()
84
+ }
72
85
}
73
86
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)))
88
95
}
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
+ }
89
104
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
+ }
97
133
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 )
101
190
}
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-" )
102
201
}
103
202
}
0 commit comments