From c8f34eba9d45c0e683adad64e7fdbf6935b6a38f Mon Sep 17 00:00:00 2001 From: senseiwells Date: Tue, 20 Feb 2024 21:09:44 +0000 Subject: [PATCH 1/2] Add important note --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 32ec4ab..0df19c8 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,8 @@ around the carpet bot will be recorded. #### Chunks +> **IMPORTANT NOTE:** While the mod will record the chunks you specify, the Minecraft client will **not** render the outermost chunks. So to record an area of **visible** chunks, you must add one chunk to your border, e.g. recording a visible area from `-5, -5` to `5, 5` you must record between `-6, -6` and `6, 6`. + To record an area of chunks on your server you can run `/replay start chunks from to in named `, for example: ``` /replay start chunks from -5 -5 to 5 5 in minecraft:overworld named MyChunkRecording From 19493d885d6b0212ce3cc7c487f811d0b0823286 Mon Sep 17 00:00:00 2001 From: senseiwells Date: Wed, 21 Feb 2024 20:13:14 +0000 Subject: [PATCH 2/2] Handle runtime resource pack loading in replays --- .../ducks/ServerReplay$PackTracker.java | 11 +++ .../replay/mixin/CommandsMixin.java | 6 ++ .../ServerCommonPacketListenerImplMixin.java | 62 ++++++++++++++++ ...rConfigurationPacketListenerImplMixin.java | 34 +++++++++ .../replay/commands/PackCommand.kt | 73 +++++++++++++++++++ .../replay/recorder/ReplayRecorder.kt | 6 +- .../RejoinConfigurationPacketListener.kt | 3 +- .../replay/rejoin/RejoinedReplayPlayer.kt | 17 ++++- src/main/resources/serverreplay.mixins.json | 2 + 9 files changed, 209 insertions(+), 5 deletions(-) create mode 100644 src/main/java/me/senseiwells/replay/ducks/ServerReplay$PackTracker.java create mode 100644 src/main/java/me/senseiwells/replay/mixin/rejoin/ServerCommonPacketListenerImplMixin.java create mode 100644 src/main/java/me/senseiwells/replay/mixin/rejoin/ServerConfigurationPacketListenerImplMixin.java create mode 100644 src/main/kotlin/me/senseiwells/replay/commands/PackCommand.kt diff --git a/src/main/java/me/senseiwells/replay/ducks/ServerReplay$PackTracker.java b/src/main/java/me/senseiwells/replay/ducks/ServerReplay$PackTracker.java new file mode 100644 index 0000000..8cab036 --- /dev/null +++ b/src/main/java/me/senseiwells/replay/ducks/ServerReplay$PackTracker.java @@ -0,0 +1,11 @@ +package me.senseiwells.replay.ducks; + +import net.minecraft.network.protocol.common.ClientboundResourcePackPushPacket; + +import java.util.Collection; + +public interface ServerReplay$PackTracker { + void replay$addPacks(Collection packs); + + Collection replay$getPacks(); +} diff --git a/src/main/java/me/senseiwells/replay/mixin/CommandsMixin.java b/src/main/java/me/senseiwells/replay/mixin/CommandsMixin.java index e3681b4..1bd3f8f 100644 --- a/src/main/java/me/senseiwells/replay/mixin/CommandsMixin.java +++ b/src/main/java/me/senseiwells/replay/mixin/CommandsMixin.java @@ -1,6 +1,8 @@ package me.senseiwells.replay.mixin; import com.mojang.brigadier.CommandDispatcher; +import me.senseiwells.replay.ServerReplay; +import me.senseiwells.replay.commands.PackCommand; import me.senseiwells.replay.commands.ReplayCommand; import net.minecraft.commands.CommandBuildContext; import net.minecraft.commands.CommandSourceStack; @@ -26,5 +28,9 @@ private void onRegisterCommands( CallbackInfo ci ) { ReplayCommand.register(this.dispatcher); + + if (ServerReplay.config.getDebug()) { + PackCommand.register(this.dispatcher); + } } } diff --git a/src/main/java/me/senseiwells/replay/mixin/rejoin/ServerCommonPacketListenerImplMixin.java b/src/main/java/me/senseiwells/replay/mixin/rejoin/ServerCommonPacketListenerImplMixin.java new file mode 100644 index 0000000..3e7bd33 --- /dev/null +++ b/src/main/java/me/senseiwells/replay/mixin/rejoin/ServerCommonPacketListenerImplMixin.java @@ -0,0 +1,62 @@ +package me.senseiwells.replay.mixin.rejoin; + +import me.senseiwells.replay.ducks.ServerReplay$PackTracker; +import net.minecraft.network.PacketSendListener; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.common.ClientboundResourcePackPopPacket; +import net.minecraft.network.protocol.common.ClientboundResourcePackPushPacket; +import net.minecraft.server.network.ServerCommonPacketListenerImpl; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.Collection; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +@Mixin(ServerCommonPacketListenerImpl.class) +public class ServerCommonPacketListenerImplMixin implements ServerReplay$PackTracker { + // We need to keep track of what packs a player has... + // We don't really care if the player accepts / declines them, we'll record them anyway. + @Unique private final Map replay$packs = new ConcurrentHashMap<>(); + + @Inject( + method = "send(Lnet/minecraft/network/protocol/Packet;Lnet/minecraft/network/PacketSendListener;)V", + at = @At("HEAD") + ) + private void onSendPacket( + Packet packet, + @Nullable PacketSendListener packetSendListener, + CallbackInfo ci + ) { + if (packet instanceof ClientboundResourcePackPushPacket resources) { + this.replay$packs.put(resources.id(), resources); + return; + } + if (packet instanceof ClientboundResourcePackPopPacket resources) { + Optional uuid = resources.id(); + if (uuid.isPresent()) { + this.replay$packs.remove(uuid.get()); + } else { + this.replay$packs.clear(); + } + } + } + + @Override + public void replay$addPacks(Collection packs) { + for (ClientboundResourcePackPushPacket packet : packs) { + this.replay$packs.put(packet.id(), packet); + } + } + + @Override + public Collection replay$getPacks() { + return this.replay$packs.values(); + } +} diff --git a/src/main/java/me/senseiwells/replay/mixin/rejoin/ServerConfigurationPacketListenerImplMixin.java b/src/main/java/me/senseiwells/replay/mixin/rejoin/ServerConfigurationPacketListenerImplMixin.java new file mode 100644 index 0000000..50174a8 --- /dev/null +++ b/src/main/java/me/senseiwells/replay/mixin/rejoin/ServerConfigurationPacketListenerImplMixin.java @@ -0,0 +1,34 @@ +package me.senseiwells.replay.mixin.rejoin; + +import com.llamalad7.mixinextras.sugar.Local; +import me.senseiwells.replay.ducks.ServerReplay$PackTracker; +import net.minecraft.network.protocol.common.ClientboundResourcePackPushPacket; +import net.minecraft.network.protocol.configuration.ServerboundFinishConfigurationPacket; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.ServerConfigurationPacketListenerImpl; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.Collection; + +@Mixin(ServerConfigurationPacketListenerImpl.class) +public class ServerConfigurationPacketListenerImplMixin { + @Inject( + method = "handleConfigurationFinished", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/network/Connection;resumeInboundAfterProtocolChange()V" + ) + ) + private void afterPlayerSpawned( + ServerboundFinishConfigurationPacket serverboundFinishConfigurationPacket, + CallbackInfo ci, + @Local ServerPlayer serverPlayer + ) { + // Merge the packs into the GamePacketListener + Collection packs = ((ServerReplay$PackTracker) this).replay$getPacks(); + ((ServerReplay$PackTracker) serverPlayer.connection).replay$addPacks(packs); + } +} diff --git a/src/main/kotlin/me/senseiwells/replay/commands/PackCommand.kt b/src/main/kotlin/me/senseiwells/replay/commands/PackCommand.kt new file mode 100644 index 0000000..69314cf --- /dev/null +++ b/src/main/kotlin/me/senseiwells/replay/commands/PackCommand.kt @@ -0,0 +1,73 @@ +package me.senseiwells.replay.commands + +import com.mojang.brigadier.Command +import com.mojang.brigadier.CommandDispatcher +import com.mojang.brigadier.arguments.StringArgumentType +import com.mojang.brigadier.context.CommandContext +import com.mojang.brigadier.suggestion.Suggestions +import com.mojang.brigadier.suggestion.SuggestionsBuilder +import me.senseiwells.replay.ducks.`ServerReplay$PackTracker` +import net.minecraft.commands.CommandSourceStack +import net.minecraft.commands.Commands +import net.minecraft.commands.SharedSuggestionProvider +import net.minecraft.commands.arguments.UuidArgument +import net.minecraft.network.protocol.common.ClientboundResourcePackPopPacket +import net.minecraft.network.protocol.common.ClientboundResourcePackPushPacket +import java.util.Optional +import java.util.UUID +import java.util.concurrent.CompletableFuture + +object PackCommand { + @JvmStatic + fun register(dispatcher: CommandDispatcher) { + dispatcher.register( + Commands.literal("resource-pack").then( + Commands.literal("push").then( + Commands.argument("url", StringArgumentType.string()).then( + Commands.argument("uuid", UuidArgument.uuid()).executes(this::pushPack) + ).executes { this.pushPack(it) } + ) + ).then( + Commands.literal("pop").then( + Commands.argument("uuid", UuidArgument.uuid()).suggests(this::suggestPacks).executes(this::popPack) + ) + ) + ) + } + + private fun pushPack(context: CommandContext): Int { + val url = StringArgumentType.getString(context, "url") + val uuid = UUID.nameUUIDFromBytes(url.encodeToByteArray()) + return this.pushPack(context, uuid) + } + + private fun pushPack( + context: CommandContext, + uuid: UUID = UuidArgument.getUuid(context, "uuid") + ): Int { + val url = StringArgumentType.getString(context, "url") + val packet = ClientboundResourcePackPushPacket(uuid, url, "", false, null) + for (player in context.source.server.playerList.players) { + player.connection.send(packet) + } + return Command.SINGLE_SUCCESS + } + + private fun popPack(context: CommandContext): Int { + val uuid = UuidArgument.getUuid(context, "uuid") + val packet = ClientboundResourcePackPopPacket(Optional.of(uuid)) + for (player in context.source.server.playerList.players) { + player.connection.send(packet) + } + return Command.SINGLE_SUCCESS + } + + private fun suggestPacks( + context: CommandContext, + builder: SuggestionsBuilder + ): CompletableFuture { + val player = context.source.player ?: return Suggestions.empty() + val packs = (player.connection as `ServerReplay$PackTracker`).`replay$getPacks`() + return SharedSuggestionProvider.suggest(packs.map { it.id.toString() }, builder) + } +} \ No newline at end of file diff --git a/src/main/kotlin/me/senseiwells/replay/recorder/ReplayRecorder.kt b/src/main/kotlin/me/senseiwells/replay/recorder/ReplayRecorder.kt index 3262253..144df40 100644 --- a/src/main/kotlin/me/senseiwells/replay/recorder/ReplayRecorder.kt +++ b/src/main/kotlin/me/senseiwells/replay/recorder/ReplayRecorder.kt @@ -436,7 +436,9 @@ abstract class ReplayRecorder( path.parent.createDirectories() val bytes = URL(packet.url).openStream().readAllBytes() path.writeBytes(bytes) - this.writeResourcePack(bytes, packet.hash, requestId) + if (!this.writeResourcePack(bytes, packet.hash, requestId)) { + ServerReplay.logger.error("Resource pack hashes do not match! Pack '${packet.url}' will not be loaded...") + } }.exceptionally { ServerReplay.logger.error("Failed to download resource pack", it) null @@ -455,7 +457,7 @@ abstract class ReplayRecorder( private fun writeResourcePack(bytes: ByteArray, expectedHash: String, id: Int): Boolean { @Suppress("DEPRECATION") val packHash = Hashing.sha1().hashBytes(bytes).toString() - if (expectedHash == packHash) { + if (expectedHash == "" || expectedHash == packHash) { this.executor.execute { try { val index = this.replay.resourcePackIndex ?: HashMap() diff --git a/src/main/kotlin/me/senseiwells/replay/rejoin/RejoinConfigurationPacketListener.kt b/src/main/kotlin/me/senseiwells/replay/rejoin/RejoinConfigurationPacketListener.kt index b7220fa..c3c39c6 100644 --- a/src/main/kotlin/me/senseiwells/replay/rejoin/RejoinConfigurationPacketListener.kt +++ b/src/main/kotlin/me/senseiwells/replay/rejoin/RejoinConfigurationPacketListener.kt @@ -31,9 +31,10 @@ class RejoinConfigurationPacketListener( // configuration checks. // We must manually pong. this.handlePong(ServerboundPongPacket(0)) - return } + } + fun runConfigurationTasks() { // We do not have to wait for the client to respond for (task in this.tasks) { task.start(this::send) diff --git a/src/main/kotlin/me/senseiwells/replay/rejoin/RejoinedReplayPlayer.kt b/src/main/kotlin/me/senseiwells/replay/rejoin/RejoinedReplayPlayer.kt index ecd150e..7f1c489 100644 --- a/src/main/kotlin/me/senseiwells/replay/rejoin/RejoinedReplayPlayer.kt +++ b/src/main/kotlin/me/senseiwells/replay/rejoin/RejoinedReplayPlayer.kt @@ -2,8 +2,8 @@ package me.senseiwells.replay.rejoin import me.senseiwells.replay.api.ReplaySenders import me.senseiwells.replay.chunk.ChunkRecorder +import me.senseiwells.replay.ducks.`ServerReplay$PackTracker` import me.senseiwells.replay.player.PlayerRecorder -import me.senseiwells.replay.player.PlayerRecorders import me.senseiwells.replay.recorder.ReplayRecorder import net.minecraft.nbt.CompoundTag import net.minecraft.network.protocol.game.* @@ -25,7 +25,10 @@ class RejoinedReplayPlayer private constructor( val connection = RejoinConnection() val cookies = CommonListenerCookie(player.gameProfile, 0, player.clientInformation()) - RejoinConfigurationPacketListener(rejoined, connection, cookies).startConfiguration() + val config = RejoinConfigurationPacketListener(rejoined, connection, cookies) + config.startConfiguration() + rejoined.sendResourcePacks() + config.runConfigurationTasks() recorder.afterConfigure() rejoined.load(player.saveWithoutId(CompoundTag())) @@ -37,6 +40,16 @@ class RejoinedReplayPlayer private constructor( this.id = this.original.id } + private fun sendResourcePacks() { + val connection = this.original.connection + // Our connection may be null if we're using a fake player + if (connection is `ServerReplay$PackTracker`) { + for (packet in connection.`replay$getPacks`()) { + this.recorder.record(packet) + } + } + } + private fun place( connection: RejoinConnection, cookies: CommonListenerCookie diff --git a/src/main/resources/serverreplay.mixins.json b/src/main/resources/serverreplay.mixins.json index 3c20ba1..810499e 100644 --- a/src/main/resources/serverreplay.mixins.json +++ b/src/main/resources/serverreplay.mixins.json @@ -25,7 +25,9 @@ "player.TrackedEntityMixin", "rejoin.ChunkMapAccessor", "rejoin.ConnectionAccessor", + "rejoin.ServerCommonPacketListenerImplMixin", "rejoin.ServerConfigurationPacketListenerImplAccessor", + "rejoin.ServerConfigurationPacketListenerImplMixin", "rejoin.ServerPlayerMixin", "rejoin.TrackedEntityAccessor", "studio.CustomViaPlatformMixin",