From 3a8d7e805b098c470bef30a3c0d80c401bbc8ae3 Mon Sep 17 00:00:00 2001 From: senseiwells Date: Mon, 23 Sep 2024 18:38:20 +0100 Subject: [PATCH 1/6] Remove c2me dependency --- libs.versions.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/libs.versions.toml b/libs.versions.toml index 7c1ff09..941554f 100644 --- a/libs.versions.toml +++ b/libs.versions.toml @@ -27,7 +27,6 @@ carpet = { module = "com.github.gnembon:fabric-carpet" , version.ref voicechat = { module = "maven.modrinth:simple-voice-chat" , version.ref = "voicechat" } voicechat-api = { module = "de.maxhenkel.voicechat:voicechat-api" , version.ref = "voicechat-api" } vmp = { module = "maven.modrinth:vmp-fabric" , version.ref = "vmp" } -c2me = { module = "maven.modrinth:c2me-fabric" , version.ref = "c2me" } servux = { module = "com.github.sakura-ryoko:servux" , version.ref = "servux" } syncmatica = { module = "com.github.sakura-ryoko:syncmatica" , version.ref = "syncmatica" } replay-studio = { module = "com.github.ReplayMod:ReplayStudio" , version.ref = "replay-studio" } From a520c5ff02b4cc7b05254d874a5b5c0c11d87916 Mon Sep 17 00:00:00 2001 From: senseiwells Date: Tue, 24 Sep 2024 01:23:51 +0100 Subject: [PATCH 2/6] Fix another compat issue with vmp --- .../mixin/compat/vmp/TrackedEntityMixin.java | 43 ------------------- src/main/resources/serverreplay.mixins.json | 1 - 2 files changed, 44 deletions(-) delete mode 100644 src/main/java/me/senseiwells/replay/mixin/compat/vmp/TrackedEntityMixin.java diff --git a/src/main/java/me/senseiwells/replay/mixin/compat/vmp/TrackedEntityMixin.java b/src/main/java/me/senseiwells/replay/mixin/compat/vmp/TrackedEntityMixin.java deleted file mode 100644 index 85441bd..0000000 --- a/src/main/java/me/senseiwells/replay/mixin/compat/vmp/TrackedEntityMixin.java +++ /dev/null @@ -1,43 +0,0 @@ -package me.senseiwells.replay.mixin.compat.vmp; - -import com.ishland.vmp.mixins.playerwatching.optimize_nearby_entity_tracking_lookups.MixinThreadedAnvilChunkStorageEntityTracker; -import com.llamalad7.mixinextras.injector.ModifyExpressionValue; -import me.senseiwells.replay.chunk.ChunkRecordable; -import me.senseiwells.replay.player.PlayerRecorders; -import net.minecraft.server.level.ChunkMap; -import net.minecraft.server.level.ServerPlayer; -import net.minecraft.world.entity.Entity; -import org.spongepowered.asm.mixin.Dynamic; -import org.spongepowered.asm.mixin.Final; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.injection.At; - -@Mixin(value = ChunkMap.TrackedEntity.class, priority = 1100) -public class TrackedEntityMixin { - @Shadow - @Final - Entity entity; - - @Dynamic(mixin = MixinThreadedAnvilChunkStorageEntityTracker.class) - @ModifyExpressionValue( - method = "tryTick", - at = @At( - value = "INVOKE", - target = "Ljava/util/Set;isEmpty()Z" - ), - remap = false - ) - private boolean shouldNotTick(boolean original) { - if (!original) { - return false; - } - if (!((ChunkRecordable) this).getRecorders().isEmpty()) { - return false; - } - if (this.entity instanceof ServerPlayer player) { - return !PlayerRecorders.has(player); - } - return true; - } -} diff --git a/src/main/resources/serverreplay.mixins.json b/src/main/resources/serverreplay.mixins.json index dc0ee6a..c0ebbb1 100644 --- a/src/main/resources/serverreplay.mixins.json +++ b/src/main/resources/serverreplay.mixins.json @@ -24,7 +24,6 @@ "compat.servux.ServuxPacketMixin", "compat.syncmatica.SyncmaticaPacketPayloadMixin", "compat.vmp.NearbyEntityTrackingMixin", - "compat.vmp.TrackedEntityMixin", "network.IdDispatchCodecAccessor", "player.ServerCommonPacketListenerImplMixin", "player.ServerConfigurationPacketListenerImplMixin", From e66c9a68224dbd830be8b926b0533c23507cc4d5 Mon Sep 17 00:00:00 2001 From: senseiwells Date: Tue, 24 Sep 2024 01:26:38 +0100 Subject: [PATCH 3/6] Revert vmp compat fix --- .../mixin/compat/vmp/TrackedEntityMixin.java | 43 +++++++++++++++++++ src/main/resources/serverreplay.mixins.json | 1 + 2 files changed, 44 insertions(+) create mode 100644 src/main/java/me/senseiwells/replay/mixin/compat/vmp/TrackedEntityMixin.java diff --git a/src/main/java/me/senseiwells/replay/mixin/compat/vmp/TrackedEntityMixin.java b/src/main/java/me/senseiwells/replay/mixin/compat/vmp/TrackedEntityMixin.java new file mode 100644 index 0000000..a4fbed8 --- /dev/null +++ b/src/main/java/me/senseiwells/replay/mixin/compat/vmp/TrackedEntityMixin.java @@ -0,0 +1,43 @@ +package me.senseiwells.replay.mixin.compat.vmp; + +import com.ishland.vmp.mixins.playerwatching.optimize_nearby_entity_tracking_lookups.MixinThreadedAnvilChunkStorageEntityTracker; +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import me.senseiwells.replay.chunk.ChunkRecordable; +import me.senseiwells.replay.player.PlayerRecorders; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import org.spongepowered.asm.mixin.Dynamic; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(value = ChunkMap.TrackedEntity.class, priority = 1100) +public class TrackedEntityMixin { + @Shadow + @Final + Entity entity; + + @Dynamic(mixin = MixinThreadedAnvilChunkStorageEntityTracker.class) + @ModifyExpressionValue( + method = "tryTick", + at = @At( + value = "INVOKE", + target = "Ljava/util/Set;isEmpty()Z" + ), + remap = false + ) + private boolean shouldNotTick(boolean original) { + if (!original) { + return false; + } + if (!((ChunkRecordable) this).getRecorders().isEmpty()) { + return false; + } + if (this.entity instanceof ServerPlayer player) { + return !PlayerRecorders.has(player); + } + return true; + } +} diff --git a/src/main/resources/serverreplay.mixins.json b/src/main/resources/serverreplay.mixins.json index b3cd6b7..19473f8 100644 --- a/src/main/resources/serverreplay.mixins.json +++ b/src/main/resources/serverreplay.mixins.json @@ -22,6 +22,7 @@ "compat.servux.ServuxPacketMixin", "compat.syncmatica.SyncmaticaPacketPayloadMixin", "compat.vmp.NearbyEntityTrackingMixin", + "compat.vmp.TrackedEntityMixin", "network.IdDispatchCodecAccessor", "player.ServerCommonPacketListenerImplMixin", "player.ServerConfigurationPacketListenerImplMixin", From f09db4f392ede7ef50fd01c08f2a7c91bd11346a Mon Sep 17 00:00:00 2001 From: senseiwells Date: Wed, 25 Sep 2024 15:19:39 +0100 Subject: [PATCH 4/6] Rework pack hosting and add option to disable pack recording --- build.gradle.kts | 3 + libs.versions.toml | 29 +++--- .../me/senseiwells/replay/ServerReplay.kt | 36 +++++-- .../replay/commands/PackCommand.kt | 1 - .../replay/commands/ReplayCommand.kt | 1 - .../replay/compat/ReplayMixinConfig.kt | 8 +- .../senseiwells/replay/config/ReplayConfig.kt | 2 + .../replay/download/DownloadHost.kt | 8 +- .../replay/recorder/ReplayRecorder.kt | 2 +- .../me/senseiwells/replay/util/HttpHost.kt | 95 ------------------- .../senseiwells/replay/viewer/ReplayViewer.kt | 65 ++++++------- .../replay/viewer/packhost/HostedPack.kt | 24 ----- .../replay/viewer/packhost/PackHost.kt | 75 --------------- .../replay/viewer/packhost/ReadablePack.kt | 43 --------- .../replay/viewer/packhost/ReplayPack.kt | 16 ---- 15 files changed, 88 insertions(+), 320 deletions(-) delete mode 100644 src/main/kotlin/me/senseiwells/replay/util/HttpHost.kt delete mode 100644 src/main/kotlin/me/senseiwells/replay/viewer/packhost/HostedPack.kt delete mode 100644 src/main/kotlin/me/senseiwells/replay/viewer/packhost/PackHost.kt delete mode 100644 src/main/kotlin/me/senseiwells/replay/viewer/packhost/ReadablePack.kt delete mode 100644 src/main/kotlin/me/senseiwells/replay/viewer/packhost/ReplayPack.kt diff --git a/build.gradle.kts b/build.gradle.kts index d49696b..a75f975 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -16,6 +16,7 @@ plugins { val shade: Configuration by configurations.creating repositories { + maven("https://maven.supersanta.me/snapshots") maven("https://maven.parchmentmc.org/") maven("https://masa.dy.fi/maven") maven("https://jitpack.io") @@ -44,6 +45,8 @@ dependencies { modImplementation(libs.fabric.api) modImplementation(libs.fabric.kotlin) + modImplementation(libs.arcade.pack.host) + modCompileOnly(libs.carpet) modCompileOnly(libs.vmp) modCompileOnly(explosion.fabric(libs.c2me)) diff --git a/libs.versions.toml b/libs.versions.toml index 751ac2c..7f52d14 100644 --- a/libs.versions.toml +++ b/libs.versions.toml @@ -10,6 +10,7 @@ parchment = "1.21:2024.07.28" fabric-api = "0.103.0+1.21.1" fabric-kotlin = "1.11.0+kotlin.2.0.0" permissions = "0.3.1" +arcade = "0.2.0-alpha.46+1.21.1" carpet = "1.4.147" voicechat = "fabric-1.21.1-2.5.20" voicechat-api = "2.5.0" @@ -20,19 +21,21 @@ syncmatica = "1.21-sakura.6" replay-studio = "1e96fda605" [libraries] -minecraft = { module = "com.mojang:minecraft" , version.ref = "minecraft" } -fabric-loader = { module = "net.fabricmc:fabric-loader" , version.ref = "fabric-loader" } -fabric-api = { module = "net.fabricmc.fabric-api:fabric-api" , version.ref = "fabric-api" } -fabric-kotlin = { module = "net.fabricmc:fabric-language-kotlin" , version.ref = "fabric-kotlin" } -permissions = { module = "me.lucko:fabric-permissions-api" , version.ref = "permissions" } -carpet = { module = "com.github.gnembon:fabric-carpet" , version.ref = "carpet" } -voicechat = { module = "maven.modrinth:simple-voice-chat" , version.ref = "voicechat" } -voicechat-api = { module = "de.maxhenkel.voicechat:voicechat-api" , version.ref = "voicechat-api" } -vmp = { module = "maven.modrinth:vmp-fabric" , version.ref = "vmp" } -c2me = { module = "maven.modrinth:c2me-fabric" , version.ref = "c2me" } -servux = { module = "com.github.sakura-ryoko:servux" , version.ref = "servux" } -syncmatica = { module = "com.github.sakura-ryoko:syncmatica" , version.ref = "syncmatica" } -replay-studio = { module = "com.github.ReplayMod:ReplayStudio" , version.ref = "replay-studio" } +minecraft = { module = "com.mojang:minecraft" , version.ref = "minecraft" } +fabric-loader = { module = "net.fabricmc:fabric-loader" , version.ref = "fabric-loader" } +fabric-api = { module = "net.fabricmc.fabric-api:fabric-api" , version.ref = "fabric-api" } +fabric-kotlin = { module = "net.fabricmc:fabric-language-kotlin" , version.ref = "fabric-kotlin" } +permissions = { module = "me.lucko:fabric-permissions-api" , version.ref = "permissions" } +replay-studio = { module = "com.github.ReplayMod:ReplayStudio" , version.ref = "replay-studio" } +arcade-pack-host = { module = "net.casual-championships:arcade-resource-pack-host", version.ref = "arcade" } + +carpet = { module = "com.github.gnembon:fabric-carpet" , version.ref = "carpet" } +voicechat = { module = "maven.modrinth:simple-voice-chat" , version.ref = "voicechat" } +voicechat-api = { module = "de.maxhenkel.voicechat:voicechat-api" , version.ref = "voicechat-api" } +vmp = { module = "maven.modrinth:vmp-fabric" , version.ref = "vmp" } +c2me = { module = "maven.modrinth:c2me-fabric" , version.ref = "c2me" } +servux = { module = "com.github.sakura-ryoko:servux" , version.ref = "servux" } +syncmatica = { module = "com.github.sakura-ryoko:syncmatica" , version.ref = "syncmatica" } [plugins] fabric-loom = { id = "fabric-loom", version.ref = "fabric-loom" } diff --git a/src/main/kotlin/me/senseiwells/replay/ServerReplay.kt b/src/main/kotlin/me/senseiwells/replay/ServerReplay.kt index d66027e..4d2a0b9 100644 --- a/src/main/kotlin/me/senseiwells/replay/ServerReplay.kt +++ b/src/main/kotlin/me/senseiwells/replay/ServerReplay.kt @@ -5,6 +5,8 @@ import me.senseiwells.replay.commands.PackCommand import me.senseiwells.replay.commands.ReplayCommand import me.senseiwells.replay.config.ReplayConfig import me.senseiwells.replay.download.DownloadHost +import net.casual.arcade.host.PackHost +import net.casual.arcade.host.pack.ReadablePack import net.fabricmc.api.ModInitializer import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents @@ -17,9 +19,10 @@ object ServerReplay: ModInitializer { const val MOD_ID = "server-replay" private var downloads: DownloadHost? = null + private var packs: PackHost? = null @JvmField - val logger: Logger = LoggerFactory.getLogger("ServerReplay") + val logger: Logger = LoggerFactory.getLogger(MOD_ID) val replay: ModContainer = FabricLoader.getInstance().getModContainer(MOD_ID).get() val version: String = this.replay.metadata.version.friendlyString @@ -29,12 +32,17 @@ object ServerReplay: ModInitializer { private set override fun onInitialize() { - this.reload() + this.config = ReplayConfig.read() ServerReplayPluginManager.loadPlugins() - ServerLifecycleEvents.SERVER_STARTING.register { this.downloads?.start() } - ServerLifecycleEvents.SERVER_STOPPING.register { this.downloads?.stop() } + ServerLifecycleEvents.SERVER_STARTING.register { + this.reloadHost() + } + ServerLifecycleEvents.SERVER_STOPPING.register { + this.downloads?.stop() + this.packs?.stop() + } CommandRegistrationCallback.EVENT.register { dispatcher, _, _ -> ReplayCommand.register(dispatcher) @@ -52,12 +60,28 @@ object ServerReplay: ModInitializer { fun reload() { this.config = ReplayConfig.read() + this.reloadHost() + } + + fun hostPack(pack: ReadablePack): PackHost.HostedPackRef? { + return this.packs?.addPack(pack) + } + + fun removePack(pack: ReadablePack) { + this.packs?.removePack(pack.name) + } + + private fun reloadHost() { + this.downloads?.stop() + this.packs?.stop() + if (this.config.allowDownloadingReplays) { val downloads = DownloadHost(this.config.replayServerIp, this.config.replayDownloadPort) downloads.start() this.downloads = downloads - } else { - this.downloads?.stop() } + val packs = PackHost(this.config.replayServerIp, this.config.replayViewerPackPort) + packs.start() + this.packs = packs } } \ No newline at end of file diff --git a/src/main/kotlin/me/senseiwells/replay/commands/PackCommand.kt b/src/main/kotlin/me/senseiwells/replay/commands/PackCommand.kt index bf187df..0ae0a02 100644 --- a/src/main/kotlin/me/senseiwells/replay/commands/PackCommand.kt +++ b/src/main/kotlin/me/senseiwells/replay/commands/PackCommand.kt @@ -17,7 +17,6 @@ import java.util.* import java.util.concurrent.CompletableFuture object PackCommand { - @JvmStatic fun register(dispatcher: CommandDispatcher) { dispatcher.register( Commands.literal("resource-pack").then( diff --git a/src/main/kotlin/me/senseiwells/replay/commands/ReplayCommand.kt b/src/main/kotlin/me/senseiwells/replay/commands/ReplayCommand.kt index eac968b..a58842b 100644 --- a/src/main/kotlin/me/senseiwells/replay/commands/ReplayCommand.kt +++ b/src/main/kotlin/me/senseiwells/replay/commands/ReplayCommand.kt @@ -33,7 +33,6 @@ import java.util.concurrent.TimeUnit import kotlin.io.path.* object ReplayCommand { - @JvmStatic fun register(dispatcher: CommandDispatcher) { dispatcher.register( Commands.literal("replay").requires { diff --git a/src/main/kotlin/me/senseiwells/replay/compat/ReplayMixinConfig.kt b/src/main/kotlin/me/senseiwells/replay/compat/ReplayMixinConfig.kt index e9b57d2..3be03aa 100644 --- a/src/main/kotlin/me/senseiwells/replay/compat/ReplayMixinConfig.kt +++ b/src/main/kotlin/me/senseiwells/replay/compat/ReplayMixinConfig.kt @@ -1,7 +1,6 @@ package me.senseiwells.replay.compat import com.google.common.collect.HashMultimap -import me.senseiwells.replay.ServerReplay import net.fabricmc.loader.api.FabricLoader import org.objectweb.asm.tree.ClassNode import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin @@ -30,15 +29,10 @@ class ReplayMixinConfig: IMixinConfigPlugin { override fun shouldApplyMixin(targetClassName: String, mixinClassName: String): Boolean { if (mixinClassName.startsWith(MIXIN_COMPAT)) { val modId = mixinClassName.removePrefix(MIXIN_COMPAT).substringBefore('.') - val isModLoaded = FabricLoader.getInstance().isModLoaded(modId) - if (!isModLoaded) { - ServerReplay.logger.debug("Not applying compat mixin for mod $modId, mod was not loaded") - } - return isModLoaded + return FabricLoader.getInstance().isModLoaded(modId) } for (modId in incompatible.get(mixinClassName)) { if (FabricLoader.getInstance().isModLoaded(modId)) { - ServerReplay.logger.debug("Not applying $mixinClassName, $modId is incompatible") return false } } diff --git a/src/main/kotlin/me/senseiwells/replay/config/ReplayConfig.kt b/src/main/kotlin/me/senseiwells/replay/config/ReplayConfig.kt index 66d60dd..b17d2b2 100644 --- a/src/main/kotlin/me/senseiwells/replay/config/ReplayConfig.kt +++ b/src/main/kotlin/me/senseiwells/replay/config/ReplayConfig.kt @@ -73,6 +73,8 @@ data class ReplayConfig( var notifyAdminsOfStatus: Boolean = true, @SerialName("fix_carpet_bot_view_distance") var fixCarpetBotViewDistance: Boolean = false, + @SerialName("include_resource_packs") + var includeResourcePacks: Boolean = true, @SerialName("ignore_sound_packets") var ignoreSoundPackets: Boolean = false, @SerialName("ignore_light_packets") diff --git a/src/main/kotlin/me/senseiwells/replay/download/DownloadHost.kt b/src/main/kotlin/me/senseiwells/replay/download/DownloadHost.kt index 8d38894..9a710aa 100644 --- a/src/main/kotlin/me/senseiwells/replay/download/DownloadHost.kt +++ b/src/main/kotlin/me/senseiwells/replay/download/DownloadHost.kt @@ -3,7 +3,7 @@ package me.senseiwells.replay.download import com.sun.net.httpserver.HttpExchange import com.sun.net.httpserver.HttpServer import me.senseiwells.replay.ServerReplay -import me.senseiwells.replay.util.HttpHost +import net.casual.arcade.host.core.HttpHost import java.io.IOException import java.net.URLDecoder import java.nio.charset.StandardCharsets @@ -11,12 +11,12 @@ import java.nio.file.Path import java.util.function.Consumer import kotlin.io.path.* -class DownloadHost(ip: String?, port: Int): HttpHost(ip, port) { +class DownloadHost(ip: String?, port: Int): HttpHost(ip, port, 3) { override fun getName(): String { - return "ReplayDownloadHost" + return "replay-download-host" } - override fun onStart(server: HttpServer, async: Consumer) { + override fun onStart(server: HttpServer) { server.createContext("/player", this::handlePlayerDownloadRequest) server.createContext("/chunk", this::handleChunkDownloadRequest) } diff --git a/src/main/kotlin/me/senseiwells/replay/recorder/ReplayRecorder.kt b/src/main/kotlin/me/senseiwells/replay/recorder/ReplayRecorder.kt index c4e5095..b459a42 100644 --- a/src/main/kotlin/me/senseiwells/replay/recorder/ReplayRecorder.kt +++ b/src/main/kotlin/me/senseiwells/replay/recorder/ReplayRecorder.kt @@ -853,7 +853,7 @@ abstract class ReplayRecorder( } private fun downloadAndRecordResourcePack(packet: ClientboundResourcePackPushPacket): Boolean { - if (packet.url.startsWith("replay://")) { + if (!ServerReplay.config.includeResourcePacks || packet.url.startsWith("replay://")) { return false } @Suppress("DEPRECATION") diff --git a/src/main/kotlin/me/senseiwells/replay/util/HttpHost.kt b/src/main/kotlin/me/senseiwells/replay/util/HttpHost.kt deleted file mode 100644 index e8d99f2..0000000 --- a/src/main/kotlin/me/senseiwells/replay/util/HttpHost.kt +++ /dev/null @@ -1,95 +0,0 @@ -package me.senseiwells.replay.util - -import com.google.common.util.concurrent.ThreadFactoryBuilder -import com.sun.net.httpserver.HttpServer -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import java.net.InetAddress -import java.net.InetSocketAddress -import java.util.* -import java.util.concurrent.CompletableFuture -import java.util.concurrent.ExecutorService -import java.util.concurrent.Executors -import java.util.function.Consumer - -abstract class HttpHost(ip: String?, port: Int?) { - protected val logger: Logger = LoggerFactory.getLogger(this.getName()) - private val builder = ThreadFactoryBuilder().setNameFormat("${this.getName()}-%d").build() - - val ip: String = ip ?: getLocalIp() - val port: Int = port ?: 8080 - - private var server: HttpServer? = null - private var pool: ExecutorService? = null - private var future: CompletableFuture? = null - - var threads: Int = 3 - - val running: Boolean - get() = this.server != null - - fun getUrl(): String { - @Suppress("HttpUrlsUsage") - return "http://${this.ip}:${this.port}" - } - - open fun getName(): String { - return this::class.java.simpleName - } - - fun start(): CompletableFuture { - this.future?.cancel(true) - - val restart = this.server !== null - this.server?.stop(0) - this.pool?.shutdownNow() - - this.pool = Executors.newFixedThreadPool(this.threads, this.builder) - val future = CompletableFuture.supplyAsync({ - try { - this.logger.info("${if (restart) "Restarting" else "Starting"} ${this.getName()}...") - - val server = HttpServer.create(InetSocketAddress("0.0.0.0", this.port), 0) - server.executor = this.pool - - val futures = LinkedList>() - this.onStart(server) { task -> - futures.add(CompletableFuture.runAsync(task, this.pool)) - } - futures.forEach { it.join() } - - server.start() - this.server = server - this.logger.info("${this.getName()} successfully started") - true - } catch (e: Exception) { - this.logger.error("Failed to start ${this.getName()}!", e) - false - } - }, this.pool) - this.future = future - return future - } - - fun stop() { - this.server?.stop(0) - this.pool?.shutdownNow() - this.future?.cancel(true) - - this.onStop() - } - - protected open fun onStart(server: HttpServer, async: Consumer) { - - } - - protected open fun onStop() { - - } - - companion object { - fun getLocalIp(): String { - return InetAddress.getLocalHost().hostAddress - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/me/senseiwells/replay/viewer/ReplayViewer.kt b/src/main/kotlin/me/senseiwells/replay/viewer/ReplayViewer.kt index ba92907..dbe2fcf 100644 --- a/src/main/kotlin/me/senseiwells/replay/viewer/ReplayViewer.kt +++ b/src/main/kotlin/me/senseiwells/replay/viewer/ReplayViewer.kt @@ -14,10 +14,8 @@ import com.replaymod.replaystudio.studio.ReplayStudio import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap import it.unimi.dsi.fastutil.ints.IntArrayList import it.unimi.dsi.fastutil.ints.IntOpenHashSet -import it.unimi.dsi.fastutil.ints.IntSets import it.unimi.dsi.fastutil.longs.LongOpenHashSet import kotlinx.coroutines.* -import kotlinx.coroutines.future.await import me.senseiwells.replay.ServerReplay import me.senseiwells.replay.ducks.PackTracker import me.senseiwells.replay.mixin.viewer.EntityInvoker @@ -29,8 +27,9 @@ import me.senseiwells.replay.viewer.ReplayViewerUtils.startViewingReplay import me.senseiwells.replay.viewer.ReplayViewerUtils.stopViewingReplay import me.senseiwells.replay.viewer.ReplayViewerUtils.toClientboundConfigurationPacket import me.senseiwells.replay.viewer.ReplayViewerUtils.toClientboundPlayPacket -import me.senseiwells.replay.viewer.packhost.PackHost -import me.senseiwells.replay.viewer.packhost.ReplayPack +import net.casual.arcade.host.HostedPack +import net.casual.arcade.host.PackHost +import net.casual.arcade.host.pack.ReadablePack import net.minecraft.ChatFormatting import net.minecraft.SharedConstants import net.minecraft.core.UUIDUtil @@ -55,6 +54,7 @@ import net.minecraft.world.scores.DisplaySlot import net.minecraft.world.scores.Objective import net.minecraft.world.scores.criteria.ObjectiveCriteria import java.io.IOException +import java.io.InputStream import java.nio.file.Path import java.util.* import java.util.function.Supplier @@ -76,8 +76,7 @@ class ReplayViewer( private var teleported = false private val coroutineScope = CoroutineScope(Dispatchers.Default + Job()) - private val packHost = PackHost(ServerReplay.config.replayServerIp, nextFreePort()) - private val packs = Int2ObjectOpenHashMap() + private val packs = Int2ObjectOpenHashMap() private var tickSpeed = 20.0F private var tickFrozen = false @@ -153,8 +152,10 @@ class ReplayViewer( } fun close() { - freePort(this.packHost.port) - this.packHost.stop() + for (hosted in this.packs.values) { + ServerReplay.removePack(hosted.pack) + } + this.coroutineScope.coroutineContext.cancelChildren() this.connection.stopViewingReplay() @@ -257,25 +258,22 @@ class ReplayViewer( return multimap } - private suspend fun hostResourcePacks() { - if (this.packHost.running) { - return - } - + private fun hostResourcePacks() { val indices = this.replay.resourcePackIndex if (indices == null || indices.isEmpty()) { return } - for (hash in indices.values) { - this.packHost.addPack(ReplayPack(hash, this.replay)) + val refs = ArrayList>() + for ((id, hash) in indices) { + val ref = ServerReplay.hostPack(ReplayPack(hash)) + if (ref != null) { + refs.add(id to ref) + } } - this.packHost.start().await() - - for ((id, hash) in indices) { - val hosted = this.packHost.getHostedPack(hash) ?: continue - this.packs[id] = hosted.url + for ((id, ref) in refs) { + this.packs[id] = ref.value } } @@ -613,7 +611,7 @@ class ReplayViewer( if (packet is ClientboundResourcePackPushPacket && packet.url.startsWith("replay://")) { val request = packet.url.removePrefix("replay://").toIntOrNull() ?: throw IllegalStateException("Malformed replay packet url") - val url = this.packs[request] + val url = this.packs[request]?.url if (url == null) { ServerReplay.logger.warn("Tried viewing unknown request $request for player ${this.player.scoreboardName}") return packet @@ -636,22 +634,21 @@ class ReplayViewer( this.connection.sendReplayPacket(packet) } - private companion object { - const val VIEWER_ID = Int.MAX_VALUE - 10 - val VIEWER_UUID: UUID = UUIDUtil.createOfflinePlayerUUID("-ViewingProfile-") - - private val active = IntSets.synchronize(IntOpenHashSet()) + private inner class ReplayPack(private val hash: String): ReadablePack { + override val name: String = "${System.identityHashCode(replay)}-${this.hash}" - fun nextFreePort(): Int { - var current = ServerReplay.config.replayViewerPackPort - while (!this.active.add(current)) { - current += 1 - } - return current + override fun readable(): Boolean { + return replay.getResourcePack(this.hash).isPresent } - fun freePort(port: Int) { - this.active.remove(port) + override fun stream(): InputStream { + return replay.getResourcePack(this.hash).orNull() + ?: throw IllegalStateException("ReplayPack ${this.hash} doesn't exist") } } + + private companion object { + const val VIEWER_ID = Int.MAX_VALUE - 10 + val VIEWER_UUID: UUID = UUIDUtil.createOfflinePlayerUUID("-ViewingProfile-") + } } \ No newline at end of file diff --git a/src/main/kotlin/me/senseiwells/replay/viewer/packhost/HostedPack.kt b/src/main/kotlin/me/senseiwells/replay/viewer/packhost/HostedPack.kt deleted file mode 100644 index 8b0bcad..0000000 --- a/src/main/kotlin/me/senseiwells/replay/viewer/packhost/HostedPack.kt +++ /dev/null @@ -1,24 +0,0 @@ -package me.senseiwells.replay.viewer.packhost - -/** - * This holds all the data for a [ReadablePack] that - * is being hosted on a pack host. - * - * @param pack The readable pack that is being hosted. - * @param url The URL that it is being hosted at. - * @param hash The hash of the [pack]. - */ -data class HostedPack( - /** - * The readable pack that is being hosted. - */ - val pack: ReadablePack, - /** - * The URL that it is being hosted at. - */ - val url: String, - /** - * The hash of the [pack]. - */ - val hash: String -) \ No newline at end of file diff --git a/src/main/kotlin/me/senseiwells/replay/viewer/packhost/PackHost.kt b/src/main/kotlin/me/senseiwells/replay/viewer/packhost/PackHost.kt deleted file mode 100644 index 077b37e..0000000 --- a/src/main/kotlin/me/senseiwells/replay/viewer/packhost/PackHost.kt +++ /dev/null @@ -1,75 +0,0 @@ -package me.senseiwells.replay.viewer.packhost - -import com.google.common.hash.Hashing -import com.sun.net.httpserver.HttpExchange -import com.sun.net.httpserver.HttpHandler -import com.sun.net.httpserver.HttpServer -import me.senseiwells.replay.util.HttpHost -import java.io.InputStream -import java.util.* -import java.util.concurrent.ConcurrentHashMap -import java.util.function.Consumer - -class PackHost(ip: String?, port: Int): HttpHost(ip, port) { - private val hosted = ConcurrentHashMap() - private val packs = ArrayList() - - init { - this.threads = 1 - } - - fun addPack(packs: ReadablePack) { - this.packs.add(packs) - } - - fun getHostedPack(name: String): HostedPack? { - val zipped = if (name.endsWith(".zip")) name else "$name.zip" - return this.hosted[zipped] - } - - override fun getName(): String { - return "ResourcePackHost" - } - - override fun onStart(server: HttpServer, async: Consumer) { - for (pack in this.packs) { - async.accept { - val name = pack.name - val url = "${this.getUrl()}/${name}" - - @Suppress("DEPRECATION") - val hash = Hashing.sha1().hashBytes(pack.stream().use(InputStream::readBytes)).toString() - - val hosted = HostedPack(pack, url, hash) - val zipped = if (pack.name.endsWith(".zip")) pack.name else "${pack.name}.zip" - this.hosted[zipped] = hosted - - server.createContext("/$name", Handler(pack)) - - this.logger.info("Hosting pack: ${pack.name}: $url") - } - } - } - - override fun onStop() { - this.packs.clear() - this.hosted.clear() - } - - private class Handler(val pack: ReadablePack): HttpHandler { - override fun handle(exchange: HttpExchange) { - if ("GET" == exchange.requestMethod && this.pack.readable()) { - exchange.responseHeaders.add("User-Agent", "Kotlin/ResourcePackHost") - exchange.sendResponseHeaders(200, this.pack.length()) - exchange.responseBody.use { response -> - this.pack.stream().use { stream -> - stream.transferTo(response) - } - } - } else { - exchange.sendResponseHeaders(400, -1) - } - exchange.close() - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/me/senseiwells/replay/viewer/packhost/ReadablePack.kt b/src/main/kotlin/me/senseiwells/replay/viewer/packhost/ReadablePack.kt deleted file mode 100644 index 039b694..0000000 --- a/src/main/kotlin/me/senseiwells/replay/viewer/packhost/ReadablePack.kt +++ /dev/null @@ -1,43 +0,0 @@ -package me.senseiwells.replay.viewer.packhost - -import java.io.InputStream - -/** - * An interface for providing a resource pack to - * a [PackHost]. - * - * @see PackHost - */ -interface ReadablePack { - /** - * The name of the pack. - * This may or may not end in `.zip`. - */ - val name: String - - /** - * This streams the contents of the [ReadablePack]. - * - * @return The [InputStream] for the pack. - */ - fun stream(): InputStream - - /** - * Checks whether the pack is currently readable. - * - * @return Whether the pack is readable. - */ - fun readable(): Boolean { - return true - } - - /** - * This gets the number of bytes of the [ReadablePack]. - * This may return 0 if the exact number is not known. - * - * @return The size of the pack in bytes. - */ - fun length(): Long { - return 0 - } -} \ No newline at end of file diff --git a/src/main/kotlin/me/senseiwells/replay/viewer/packhost/ReplayPack.kt b/src/main/kotlin/me/senseiwells/replay/viewer/packhost/ReplayPack.kt deleted file mode 100644 index 768a294..0000000 --- a/src/main/kotlin/me/senseiwells/replay/viewer/packhost/ReplayPack.kt +++ /dev/null @@ -1,16 +0,0 @@ -package me.senseiwells.replay.viewer.packhost - -import com.replaymod.replaystudio.replay.ReplayFile -import java.io.InputStream - -class ReplayPack( - hash: String, - private val replay: ReplayFile -): ReadablePack { - override val name: String = hash - - override fun stream(): InputStream { - return this.replay.getResourcePack(this.name).orNull() - ?: throw IllegalStateException("ReplayPack ${this.name} doesn't exist") - } -} \ No newline at end of file From b4689897fca4273b0b8943cf285a53b88928e3ce Mon Sep 17 00:00:00 2001 From: senseiwells Date: Wed, 25 Sep 2024 15:26:19 +0100 Subject: [PATCH 5/6] Update README --- README.md | 4 +++- build.gradle.kts | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index bb6b1ba..82c20e4 100644 --- a/README.md +++ b/README.md @@ -223,6 +223,7 @@ After you boot the server a new file will be generated in the path "pause_notify_players": true, "notify_admins_of_status": true, "fix_carpet_bot_view_distance": false, + "include_resource_packs": true, "ignore_sound_packets": false, "ignore_light_packets": true, "ignore_chat_packets": false, @@ -261,6 +262,7 @@ After you boot the server a new file will be generated in the path | `"pause_notify_players"` |

If `pause_unloaded_chunks` is enabled and this is enabled then when the recording for the chunk area is paused or resumed all online players will be notified.

| | `"notify_admins_of_status"` |

When enabled this will notify admins of when a replay starts, when a replay ends, and when a replay has finished saving, as well as any errors that occur.

| | `"fix_carpet_bot_view_distance"` |

If you are recording carpet bots you want to enable this as it sets the view distance to the server view distance. Otherwise it will only record a distance of 2 chunks around the bot.

| +| `"include_resource_packs"` |

If enabled all server-side resource packs will be copied in the replay file to ensure correct playback. Disabling this will decrease file size but instead it'll try to download the pack from the original source whenever viewing the replay, there is no guarantee that this will work correctly.

| | `"ignore_sound_packets"` |

If you are recording a large area for a timelapse it's unlikely you'll want to record any sounds, these can eat up significant storage space.

| | `"ignore_light_packets"` |

Light is calculated on the client as well as on the server so light packets are mostly redundant.

| | `"ignore_chat_packets"` |

Stops chat packets (from both the server and other players) from being recorded if they are not necessary for your replay.

| @@ -450,7 +452,7 @@ repositories { } dependencies { - modImplementation("me.senseiwells:server-replay:1.1.4+1.21.1") + modImplementation("me.senseiwells:server-replay:1.1.5+1.21.1") } ``` diff --git a/build.gradle.kts b/build.gradle.kts index a75f975..952f4b4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -28,7 +28,7 @@ repositories { } -val modVersion = "1.1.4" +val modVersion = "1.1.5" val releaseVersion = "${modVersion}+mc${libs.versions.minecraft.get()}" version = releaseVersion group = "me.senseiwells" @@ -45,7 +45,7 @@ dependencies { modImplementation(libs.fabric.api) modImplementation(libs.fabric.kotlin) - modImplementation(libs.arcade.pack.host) + include(modImplementation(libs.arcade.pack.host.get())!!) modCompileOnly(libs.carpet) modCompileOnly(libs.vmp) From 04aad600db3705026759d91eba96554493d7dfbd Mon Sep 17 00:00:00 2001 From: senseiwells Date: Wed, 25 Sep 2024 15:29:30 +0100 Subject: [PATCH 6/6] Update Changelog --- build.gradle.kts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 952f4b4..e0e46bc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -117,9 +117,8 @@ tasks { file = remapJar.get().archiveFile changelog.set( """ - - Fixed an incompatibility with Servux - - Fixed an incompatibility with C2ME - - Fixed an issue that caused players to not be able to join when auto recording + - Added `"include_resource_packs"` config + - Optimized resource pack hosting """.trimIndent() ) type = STABLE