diff --git a/LICENSE b/LICENSE index d715525..891d111 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 senseiwells +Copyright (c) 2024 senseiwells Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/build.gradle.kts b/build.gradle.kts index adb6bf6..f6c7875 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -33,6 +33,7 @@ dependencies { }) modImplementation("net.fabricmc:fabric-loader:${property("loader_version")}") + modImplementation("net.fabricmc:fabric-language-kotlin:${property("fabric_kotlin_version")}") modImplementation("net.fabricmc.fabric-api:fabric-api:${property("fabric_version")}") // I've had some issues with ReplayStudio and slf4j (in dev) @@ -44,7 +45,6 @@ dependencies { exclude(group = "com.google.guava", module = "guava") exclude(group = "com.google.code.gson", module = "gson") }) - include(modImplementation("net.fabricmc:fabric-language-kotlin:${property("fabric_kotlin_version")}")!!) include(modImplementation("me.lucko:fabric-permissions-api:${property("permissions_version")}")!!) modImplementation("com.github.gnembon:fabric-carpet:${property("carpet_version")}") diff --git a/src/main/java/me/senseiwells/replay/ducks/ServerReplay$ChunkMapInvoker.java b/src/main/java/me/senseiwells/replay/ducks/ServerReplay$ChunkMapInvoker.java index e576996..90a8c40 100644 --- a/src/main/java/me/senseiwells/replay/ducks/ServerReplay$ChunkMapInvoker.java +++ b/src/main/java/me/senseiwells/replay/ducks/ServerReplay$ChunkMapInvoker.java @@ -7,8 +7,8 @@ public interface ServerReplay$ChunkMapInvoker extends ChunkMapInvoker { @Override - default ChunkHolder getVisibleChunkIfExists(long pos) { - return this.replay$getVisibleChunkIfPresent(pos); + default ChunkHolder getUpdatingChunkIfPresent(long pos) { + return this.replay$getUpdatingChunkIfPresent(pos); } @NotNull @@ -17,7 +17,7 @@ default ThreadedLevelLightEngine getLightEngine() { return this.replay$getLightEngine(); } - ChunkHolder replay$getVisibleChunkIfPresent(long pos); + ChunkHolder replay$getUpdatingChunkIfPresent(long pos); ThreadedLevelLightEngine replay$getLightEngine(); } diff --git a/src/main/java/me/senseiwells/replay/mixin/MinecraftServerMixin.java b/src/main/java/me/senseiwells/replay/mixin/MinecraftServerMixin.java index 49a4e20..b6d9c29 100644 --- a/src/main/java/me/senseiwells/replay/mixin/MinecraftServerMixin.java +++ b/src/main/java/me/senseiwells/replay/mixin/MinecraftServerMixin.java @@ -1,13 +1,17 @@ package me.senseiwells.replay.mixin; +import me.senseiwells.replay.chunk.ChunkRecorder; +import me.senseiwells.replay.chunk.ChunkRecorders; import me.senseiwells.replay.config.ReplayConfig; import me.senseiwells.replay.player.PlayerRecorder; import me.senseiwells.replay.player.PlayerRecorders; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; import net.minecraft.server.MinecraftServer; 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 org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; @Mixin(MinecraftServer.class) public class MinecraftServerMixin { @@ -24,14 +28,29 @@ private void onServerLoaded(CallbackInfo ci) { } @Inject( - method = "stopServer", + method = "saveAllChunks", at = @At("TAIL") ) - private void onServerStopped(CallbackInfo ci) { + private void onSave( + boolean suppressLog, + boolean flush, + boolean forced, + CallbackInfoReturnable cir + ) { ReplayConfig.write(); + } + @Inject( + method = "stopServer", + at = @At("TAIL") + ) + private void onServerStopped(CallbackInfo ci) { for (PlayerRecorder recorder : PlayerRecorders.all()) { recorder.stop(); } + + for (ChunkRecorder recorder : ChunkRecorders.all()) { + recorder.stop(); + } } } diff --git a/src/main/java/me/senseiwells/replay/mixin/ServerLoginPacketListenerImplMixin.java b/src/main/java/me/senseiwells/replay/mixin/ServerLoginPacketListenerImplMixin.java index 3ca1165..6db28ae 100644 --- a/src/main/java/me/senseiwells/replay/mixin/ServerLoginPacketListenerImplMixin.java +++ b/src/main/java/me/senseiwells/replay/mixin/ServerLoginPacketListenerImplMixin.java @@ -35,9 +35,9 @@ private void onLoggedIn( CallbackInfo ci ) { GameProfile profile = this.authenticatedProfile; - if (profile != null && ReplayConfig.getEnabled() && PlayerRecorders.predicate.test(new ReplayPlayerContext(this.server, profile))) { - ServerReplay.logger.info("Started to record player '{}'", profile.getName()); + if (profile != null && ReplayConfig.getEnabled() && ReplayConfig.predicate.test(new ReplayPlayerContext(this.server, profile))) { PlayerRecorder recorder = PlayerRecorders.create(this.server, profile); + recorder.logStart(); recorder.afterLogin(); } } diff --git a/src/main/java/me/senseiwells/replay/mixin/ChunkHolderMixin.java b/src/main/java/me/senseiwells/replay/mixin/chunk/ChunkHolderMixin.java similarity index 97% rename from src/main/java/me/senseiwells/replay/mixin/ChunkHolderMixin.java rename to src/main/java/me/senseiwells/replay/mixin/chunk/ChunkHolderMixin.java index bde4a04..293a867 100644 --- a/src/main/java/me/senseiwells/replay/mixin/ChunkHolderMixin.java +++ b/src/main/java/me/senseiwells/replay/mixin/chunk/ChunkHolderMixin.java @@ -1,4 +1,4 @@ -package me.senseiwells.replay.mixin; +package me.senseiwells.replay.mixin.chunk; import com.llamalad7.mixinextras.injector.ModifyExpressionValue; import me.senseiwells.replay.chunk.ChunkRecorder; diff --git a/src/main/java/me/senseiwells/replay/mixin/chunk/ChunkMapMixin.java b/src/main/java/me/senseiwells/replay/mixin/chunk/ChunkMapMixin.java new file mode 100644 index 0000000..e0f5c28 --- /dev/null +++ b/src/main/java/me/senseiwells/replay/mixin/chunk/ChunkMapMixin.java @@ -0,0 +1,64 @@ +package me.senseiwells.replay.mixin.chunk; + +import com.llamalad7.mixinextras.sugar.Local; +import me.senseiwells.replay.chunk.ChunkRecorder; +import me.senseiwells.replay.chunk.ChunkRecorders; +import me.senseiwells.replay.config.ReplayConfig; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.world.level.ChunkPos; +import org.spongepowered.asm.mixin.Debug; +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 org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.function.BooleanSupplier; + +@Debug(export = true) +@Mixin(ChunkMap.class) +public class ChunkMapMixin { + @Inject( + method = "updateChunkScheduling", + at = @At( + value = "INVOKE", + target = "Lit/unimi/dsi/fastutil/longs/Long2ObjectLinkedOpenHashMap;put(JLjava/lang/Object;)Ljava/lang/Object;", + remap = false + ) + ) + private void onUpdateChunkMap( + long chunkPos, + int newLevel, + ChunkHolder holder, + int oldLevel, + CallbackInfoReturnable cir + ) { + ChunkPos pos = new ChunkPos(chunkPos); + for (ChunkRecorder recorder : ChunkRecorders.all()) { + if (recorder.getChunks().contains(pos)) { + recorder.unpause(pos); + } + } + } + + @Inject( + method = "processUnloads", + at = @At( + value = "INVOKE", + target = "Lit/unimi/dsi/fastutil/longs/Long2ObjectLinkedOpenHashMap;put(JLjava/lang/Object;)Ljava/lang/Object;", + remap = false + ) + ) + private void onUnloadChunk( + BooleanSupplier hasMoreTime, + CallbackInfo ci, + @Local ChunkHolder holder + ) { + if (ReplayConfig.getSkipWhenChunksUnloaded()) { + for (ChunkRecorder recorder : ChunkRecorders.all()) { + recorder.pause(holder.getPos()); + } + } + } +} diff --git a/src/main/java/me/senseiwells/replay/mixin/PlayerListMixin.java b/src/main/java/me/senseiwells/replay/mixin/chunk/PlayerListMixin.java similarity index 98% rename from src/main/java/me/senseiwells/replay/mixin/PlayerListMixin.java rename to src/main/java/me/senseiwells/replay/mixin/chunk/PlayerListMixin.java index 7f5b397..96bd539 100644 --- a/src/main/java/me/senseiwells/replay/mixin/PlayerListMixin.java +++ b/src/main/java/me/senseiwells/replay/mixin/chunk/PlayerListMixin.java @@ -1,4 +1,4 @@ -package me.senseiwells.replay.mixin; +package me.senseiwells.replay.mixin.chunk; import me.senseiwells.replay.chunk.ChunkRecorder; import me.senseiwells.replay.chunk.ChunkRecorders; diff --git a/src/main/java/me/senseiwells/replay/mixin/chunk/ServerChunkCacheInvoker.java b/src/main/java/me/senseiwells/replay/mixin/chunk/ServerChunkCacheInvoker.java new file mode 100644 index 0000000..1124c94 --- /dev/null +++ b/src/main/java/me/senseiwells/replay/mixin/chunk/ServerChunkCacheInvoker.java @@ -0,0 +1,22 @@ +package me.senseiwells.replay.mixin.chunk; + +import com.mojang.datafixers.util.Either; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkStatus; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +import java.util.concurrent.CompletableFuture; + +@Mixin(ServerChunkCache.class) +public interface ServerChunkCacheInvoker { + @Invoker("getChunkFutureMainThread") + CompletableFuture> getChunkAsync( + int x, + int y, + ChunkStatus status, + boolean load + ); +} diff --git a/src/main/java/me/senseiwells/replay/mixin/rejoin/ChunkMapMixin.java b/src/main/java/me/senseiwells/replay/mixin/rejoin/ChunkMapMixin.java index e93fbc7..ad8932a 100644 --- a/src/main/java/me/senseiwells/replay/mixin/rejoin/ChunkMapMixin.java +++ b/src/main/java/me/senseiwells/replay/mixin/rejoin/ChunkMapMixin.java @@ -12,13 +12,11 @@ @Mixin(ChunkMap.class) public abstract class ChunkMapMixin implements ServerReplay$ChunkMapInvoker { - @Nullable @Shadow protected abstract ChunkHolder getVisibleChunkIfPresent(long chunkPos); - @Shadow @Final private ThreadedLevelLightEngine lightEngine; @Override - public ChunkHolder replay$getVisibleChunkIfPresent(long pos) { - return this.getVisibleChunkIfPresent(pos); + public ChunkHolder replay$getUpdatingChunkIfPresent(long pos) { + return this.getUpdatingChunkIfPresent(pos); } @NotNull diff --git a/src/main/kotlin/me/senseiwells/replay/chunk/ChunkArea.kt b/src/main/kotlin/me/senseiwells/replay/chunk/ChunkArea.kt index 250f978..9af3ed9 100644 --- a/src/main/kotlin/me/senseiwells/replay/chunk/ChunkArea.kt +++ b/src/main/kotlin/me/senseiwells/replay/chunk/ChunkArea.kt @@ -35,8 +35,23 @@ class ChunkArea( } override fun iterator(): Iterator { - // TODO: Implement this faster - return ChunkPos.rangeClosed(this.from, this.to).iterator() + val dx = this.to.x - this.from.x + 1 + val dz = this.to.z - this.from.z + 1 + val total = dx * dz + return object: Iterator { + private var index = 0 + + override fun hasNext(): Boolean { + return this.index < total + } + + override fun next(): ChunkPos { + val x = this.index % dx + val z = this.index / dx + this.index++ + return ChunkPos(from.x + x, from.z + z) + } + } } override fun equals(other: Any?): Boolean { diff --git a/src/main/kotlin/me/senseiwells/replay/chunk/ChunkRecorder.kt b/src/main/kotlin/me/senseiwells/replay/chunk/ChunkRecorder.kt index 793495e..071eaea 100644 --- a/src/main/kotlin/me/senseiwells/replay/chunk/ChunkRecorder.kt +++ b/src/main/kotlin/me/senseiwells/replay/chunk/ChunkRecorder.kt @@ -1,9 +1,13 @@ package me.senseiwells.replay.chunk +import me.senseiwells.replay.config.ReplayConfig +import me.senseiwells.replay.mixin.rejoin.ChunkMapAccessor import me.senseiwells.replay.recorder.ChunkSender import me.senseiwells.replay.recorder.ReplayRecorder import me.senseiwells.replay.rejoin.RejoinedReplayPlayer +import me.senseiwells.replay.util.ducks.ChunkMapInvoker import net.minecraft.core.UUIDUtil +import net.minecraft.network.chat.Component import net.minecraft.network.protocol.Packet import net.minecraft.network.protocol.game.ClientboundAddEntityPacket import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket @@ -16,13 +20,11 @@ import net.minecraft.world.entity.EntityType import net.minecraft.world.level.ChunkPos import net.minecraft.world.level.levelgen.Heightmap import net.minecraft.world.phys.Vec3 +import org.jetbrains.annotations.ApiStatus.Internal import java.nio.file.Path import java.util.concurrent.CompletableFuture import java.util.function.Consumer -// TODO: -// - When everything is unloaded in the chunk area then we skip forward... -// - What happens when chunks are initially loaded? class ChunkRecorder internal constructor( val chunks: ChunkArea, val recorderName: String, @@ -30,6 +32,9 @@ class ChunkRecorder internal constructor( ): ReplayRecorder(chunks.level.server, PROFILE,recordings), ChunkSender { private val dummy = ServerPlayer(this.server, this.chunks.level, PROFILE, ClientInformation.createDefault()) + private var totalPausedTime: Long = 0 + private var lastPaused: Long = 0 + override val level: ServerLevel get() = this.chunks.level @@ -38,13 +43,10 @@ class ChunkRecorder internal constructor( } override fun start(): Boolean { - // We load all the chunks to ensure we can - // correctly record the initial chunks. - for (pos in this.chunks) { - this.level.chunkSource.addRegionTicket(ChunkRecorders.RECORDING_TICKET, pos, 1, pos) - } - val center = this.getCenterChunk() + // Load the chunk + this.level.getChunk(center.x, center.z) + val x = center.middleBlockX val z = center.middleBlockZ val y = this.level.getHeight(Heightmap.Types.WORLD_SURFACE, x, z) @@ -87,6 +89,10 @@ class ChunkRecorder internal constructor( } } + override fun getTimestamp(): Long { + return super.getTimestamp() - this.totalPausedTime - this.getCurrentPause() + } + override fun canContinueRecording(): Boolean { return true } @@ -115,7 +121,57 @@ class ChunkRecorder internal constructor( (tracking as ChunkRecorderTrackedEntity).addRecorder(this) } + @Internal + fun pause(unloaded: ChunkPos) { + if (!this.paused() && ReplayConfig.skipWhenChunksUnloaded && this.chunks.contains(unloaded)) { + for (pos in this.chunks) { + if (this.level.chunkSource.hasChunk(pos.x, pos.z)) { + return + } + } + this.lastPaused = System.currentTimeMillis() + + if (ReplayConfig.notifyPlayersLoadingChunks) { + this.ignore { + this.server.playerList.broadcastSystemMessage( + Component.literal("Paused recording for ${this.getName()}, chunks were unloaded"), + false + ) + } + } + } + } + + @Internal + fun unpause(loaded: ChunkPos) { + if (this.paused() && this.chunks.contains(loaded)) { + this.totalPausedTime += this.getCurrentPause() + this.lastPaused = 0L + + if (ReplayConfig.notifyPlayersLoadingChunks) { + this.ignore { + this.server.playerList.broadcastSystemMessage( + Component.literal("Resumed recording for ${this.getName()}, chunks were loaded"), + false + ) + } + } + } + } + + @Internal + internal fun paused(): Boolean { + return this.lastPaused != 0L + } + + private fun getCurrentPause(): Long { + if (this.paused()) { + return System.currentTimeMillis() - this.lastPaused + } + return 0L + } + companion object { - private val PROFILE = UUIDUtil.createOfflineProfile("ChunkRecorder") + private val PROFILE = UUIDUtil.createOfflineProfile("CR") } } \ No newline at end of file diff --git a/src/main/kotlin/me/senseiwells/replay/commands/ReplayCommand.kt b/src/main/kotlin/me/senseiwells/replay/commands/ReplayCommand.kt index 3d30c42..e72234c 100644 --- a/src/main/kotlin/me/senseiwells/replay/commands/ReplayCommand.kt +++ b/src/main/kotlin/me/senseiwells/replay/commands/ReplayCommand.kt @@ -116,7 +116,7 @@ object ReplayCommand { context.source.sendSuccess({ Component.literal("ServerReplay is now enabled!") }, true) for (player in context.source.server.playerList.players) { - if (PlayerRecorders.predicate.test(ReplayPlayerContext.of(player))) { + if (ReplayConfig.predicate.test(ReplayPlayerContext.of(player))) { PlayerRecorders.create(player).tryStart() } } @@ -257,7 +257,7 @@ object ReplayCommand { if (recorders.isNotEmpty()) { builder.append("Currently Recording $type:").append("\n") for ((recorder, compressed) in recorders.map { it to it.getCompressedRecordingSize() }) { - val seconds = recorder.getRecordingTimeMS() / 1000 + val seconds = recorder.getTotalRecordingTime() / 1000 val hours = seconds / 3600 val minutes = seconds % 3600 / 60 val secs = seconds % 60 diff --git a/src/main/kotlin/me/senseiwells/replay/config/ReplayConfig.kt b/src/main/kotlin/me/senseiwells/replay/config/ReplayConfig.kt index 84628f9..7dc1839 100644 --- a/src/main/kotlin/me/senseiwells/replay/config/ReplayConfig.kt +++ b/src/main/kotlin/me/senseiwells/replay/config/ReplayConfig.kt @@ -22,6 +22,10 @@ object ReplayConfig { @JvmStatic var enabled: Boolean = false + @JvmStatic + var skipWhenChunksUnloaded = false + @JvmStatic + var notifyPlayersLoadingChunks = true var worldName = "World" var serverName = "Server" var maxFileSizeString = "0GB" @@ -31,10 +35,12 @@ object ReplayConfig { var chunkRecordingPath: Path = FabricLoader.getInstance().gameDir.resolve("recordings").resolve("chunks") var playerRecordingPath: Path = FabricLoader.getInstance().gameDir.resolve("recordings").resolve("players") + @JvmField val predicate = Predicate { this.reloadablePredicate.shouldRecord(it) } @JvmStatic fun read() { + // TODO: Make this better xD try { val path = this.getPath() if (!path.exists()) { @@ -64,6 +70,12 @@ object ReplayConfig { if (json.has("restart_after_max_file_size")) { this.restartAfterMaxFileSize = json.get("restart_after_max_file_size").asBoolean } + if (json.has("pause_unloaded_chunks")) { + this.skipWhenChunksUnloaded = json.get("pause_unloaded_chunks").asBoolean + } + if (json.has("pause_notify_players")) { + this.notifyPlayersLoadingChunks = json.get("pause_notify_players").asBoolean + } if (json.has("recording_path")) { this.playerRecordingPath = Path.of(json.get("recording_path").asString) } @@ -73,7 +85,7 @@ object ReplayConfig { if (json.has("chunk_recording_path")) { this.chunkRecordingPath = Path.of(json.get("chunk_recording_path").asString) } - if (json.has("predicate")) { + if (json.has("player_predicate")) { this.reloadablePredicate = this.deserializePlayerPredicate(json.getAsJsonObject("predicate")) } } catch (e: Exception) { @@ -88,11 +100,13 @@ object ReplayConfig { json.addProperty("enabled", this.enabled) json.addProperty("world_name", this.worldName) json.addProperty("server_name", this.serverName) - json.addProperty("max_raw_file_size", this.maxFileSizeString) + json.addProperty("max_file_size", this.maxFileSizeString) json.addProperty("restart_after_max_file_size", this.restartAfterMaxFileSize) + json.addProperty("pause_unloaded_chunks", this.skipWhenChunksUnloaded) + json.addProperty("pause_notify_players", this.notifyPlayersLoadingChunks) json.addProperty("player_recording_path", this.playerRecordingPath.pathString) json.addProperty("chunk_recording_path", this.chunkRecordingPath.pathString) - json.add("predicate", this.reloadablePredicate.serialise()) + json.add("player_predicate", this.reloadablePredicate.serialise()) val path = this.getPath() path.parent.createDirectories() path.bufferedWriter().use { diff --git a/src/main/kotlin/me/senseiwells/replay/player/PlayerRecorders.kt b/src/main/kotlin/me/senseiwells/replay/player/PlayerRecorders.kt index a1e3020..6be0fa7 100644 --- a/src/main/kotlin/me/senseiwells/replay/player/PlayerRecorders.kt +++ b/src/main/kotlin/me/senseiwells/replay/player/PlayerRecorders.kt @@ -14,9 +14,6 @@ object PlayerRecorders { private val players = LinkedHashMap() private val closing = HashMap>() - @JvmField - var predicate = ReplayConfig.predicate - @JvmStatic fun create(player: ServerPlayer): PlayerRecorder { if (player is RejoinedReplayPlayer) { diff --git a/src/main/kotlin/me/senseiwells/replay/recorder/ChunkSender.kt b/src/main/kotlin/me/senseiwells/replay/recorder/ChunkSender.kt index 40c41a9..a6dc079 100644 --- a/src/main/kotlin/me/senseiwells/replay/recorder/ChunkSender.kt +++ b/src/main/kotlin/me/senseiwells/replay/recorder/ChunkSender.kt @@ -3,6 +3,7 @@ package me.senseiwells.replay.recorder import it.unimi.dsi.fastutil.ints.IntOpenHashSet import it.unimi.dsi.fastutil.ints.IntSet import me.senseiwells.replay.ServerReplay +import me.senseiwells.replay.mixin.chunk.ServerChunkCacheInvoker import me.senseiwells.replay.mixin.rejoin.ChunkMapAccessor import me.senseiwells.replay.mixin.rejoin.TrackedEntityAccessor import me.senseiwells.replay.util.ducks.ChunkMapInvoker @@ -14,7 +15,6 @@ import net.minecraft.network.protocol.game.ClientboundSetEntityLinkPacket import net.minecraft.network.protocol.game.ClientboundSetPassengersPacket import net.minecraft.server.level.ChunkMap import net.minecraft.server.level.ChunkMap.TrackedEntity -import net.minecraft.server.level.ServerEntity import net.minecraft.server.level.ServerLevel import net.minecraft.world.entity.Entity import net.minecraft.world.entity.Mob @@ -55,17 +55,15 @@ interface ChunkSender { this.sendPacket(ClientboundSetChunkCacheCenterPacket(center.x, center.z)) - val chunks = this.level.chunkSource.chunkMap - chunks as ChunkMapInvoker + val source = this.level.chunkSource + source as ServerChunkCacheInvoker + val chunks = source.chunkMap this.forEachChunk { pos -> - val holder = chunks.getVisibleChunkIfExists(pos.toLong()) - if (holder == null) { - ServerReplay.logger.warn("Chunk is not loaded at $pos, failed to send") + val chunk = source.getChunk(pos.x, pos.z, true) + if (chunk != null) { + this.sendChunk(chunks, chunk, seen) } else { - val chunk = holder.fullChunk - if (chunk != null) { - this.sendChunk(chunks, chunk, seen) - } + ServerReplay.logger.warn("Failed to get chunk at $pos, didn't send") } } } diff --git a/src/main/kotlin/me/senseiwells/replay/recorder/ReplayRecorder.kt b/src/main/kotlin/me/senseiwells/replay/recorder/ReplayRecorder.kt index 4c17a2d..ddd23d5 100644 --- a/src/main/kotlin/me/senseiwells/replay/recorder/ReplayRecorder.kt +++ b/src/main/kotlin/me/senseiwells/replay/recorder/ReplayRecorder.kt @@ -53,7 +53,7 @@ abstract class ReplayRecorder( private val output: ReplayOutputStream private val meta: ReplayMetaData - private val start: Long + private var start: Long = 0 private var protocol = ConnectionProtocol.LOGIN private var lastPacket = 0L @@ -63,6 +63,8 @@ abstract class ReplayRecorder( private var started = false + private var ignore = false + val stopped: Boolean get() = this.executor.isShutdown val recordingPlayerUUID: UUID @@ -77,12 +79,14 @@ abstract class ReplayRecorder( this.output = this.replay.writePacketData() this.meta = this.createNewMeta() - this.start = System.currentTimeMillis() - this.saveMeta() } fun record(outgoing: MinecraftPacket<*>) { + if (this.ignore) { + return + } + if (!this.started) { throw IllegalStateException("Cannot record packets if recorder not started") } @@ -116,7 +120,7 @@ abstract class ReplayRecorder( buf.release() } - val timestamp = this.getRecordingTimeMS() + val timestamp = this.getTimestamp() this.lastPacket = timestamp this.executor.execute { @@ -137,13 +141,17 @@ abstract class ReplayRecorder( } if (this.start()) { if (log) { - ServerReplay.logger.info("Started replay for ${this.getName()}") + this.logStart() } return true } return false } + fun logStart() { + ServerReplay.logger.info("Started replay for ${this.getName()}") + } + @JvmOverloads fun stop(save: Boolean = true): CompletableFuture { if (this.stopped) { @@ -156,7 +164,7 @@ abstract class ReplayRecorder( return future } - fun getRecordingTimeMS(): Long { + fun getTotalRecordingTime(): Long { return System.currentTimeMillis() - this.start } @@ -171,6 +179,8 @@ abstract class ReplayRecorder( @Internal fun afterLogin() { this.started = true + this.start = System.currentTimeMillis() + // We will not have recorded this, so we need to do it manually. this.record(ClientboundGameProfilePacket(this.profile)) @@ -182,6 +192,20 @@ abstract class ReplayRecorder( this.protocol = ConnectionProtocol.PLAY } + protected fun ignore(block: () -> Unit) { + val previous = this.ignore + try { + this.ignore = true + block() + } finally { + this.ignore = previous + } + } + + open fun getTimestamp(): Long { + return this.getTotalRecordingTime() + } + abstract fun getName(): String protected abstract fun start(): Boolean diff --git a/src/main/kotlin/me/senseiwells/replay/util/ducks/ChunkMapInvoker.kt b/src/main/kotlin/me/senseiwells/replay/util/ducks/ChunkMapInvoker.kt index 0d3a287..edd6afa 100644 --- a/src/main/kotlin/me/senseiwells/replay/util/ducks/ChunkMapInvoker.kt +++ b/src/main/kotlin/me/senseiwells/replay/util/ducks/ChunkMapInvoker.kt @@ -4,7 +4,7 @@ import net.minecraft.server.level.ChunkHolder import net.minecraft.server.level.ThreadedLevelLightEngine interface ChunkMapInvoker { - fun getVisibleChunkIfExists(pos: Long): ChunkHolder? + fun getUpdatingChunkIfPresent(pos: Long): ChunkHolder? fun getLightEngine(): ThreadedLevelLightEngine } \ No newline at end of file diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 6ee0657..3de2bb1 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, - "id": "serverreplay", + "id": "server-replay", "version": "${version}", "name": "Server Replay Mod", "description": "", diff --git a/src/main/resources/serverreplay.mixins.json b/src/main/resources/serverreplay.mixins.json index 7a35a88..31e3b5f 100644 --- a/src/main/resources/serverreplay.mixins.json +++ b/src/main/resources/serverreplay.mixins.json @@ -5,10 +5,12 @@ "compatibilityLevel": "JAVA_17", "plugin": "me.senseiwells.replay.util.ReplayMixinConfig", "mixins": [ - "ChunkHolderMixin", + "chunk.ChunkHolderMixin", + "chunk.ChunkMapMixin", "CommandsMixin", "MinecraftServerMixin", - "PlayerListMixin", + "chunk.PlayerListMixin", + "chunk.ServerChunkCacheInvoker", "ServerCommonPacketListenerImplMixin", "ServerConfigurationPacketListenerImplMixin", "ServerLevelMixin",