From cadc126902cbb4d4c20d30d35b3bff9e8ad75871 Mon Sep 17 00:00:00 2001 From: Lumine1909 <133463833+Lumine1909@users.noreply.github.com> Date: Tue, 29 Jul 2025 06:03:18 -0700 Subject: [PATCH] Fix replay api and add null check in botlist (#627) * Fix replay api and add null check in botlist * Update botlist fix * Fix bugs * All done! * Clean up --- .../java/org/leavesmc/leaves/bot/BotList.java | 14 ++- .../org/leavesmc/leaves/bot/ServerBot.java | 6 + .../org/leavesmc/leaves/replay/Recorder.java | 107 +++++++++--------- .../leavesmc/leaves/replay/ReplayFile.java | 6 +- .../leaves/replay/ServerPhotographer.java | 6 + 5 files changed, 83 insertions(+), 56 deletions(-) diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bot/BotList.java b/leaves-server/src/main/java/org/leavesmc/leaves/bot/BotList.java index 4d7af942..4437f826 100644 --- a/leaves-server/src/main/java/org/leavesmc/leaves/bot/BotList.java +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bot/BotList.java @@ -36,6 +36,7 @@ import org.leavesmc.leaves.event.bot.BotRemoveEvent; import org.leavesmc.leaves.event.bot.BotSpawnLocationEvent; import org.slf4j.Logger; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Locale; @@ -204,7 +205,7 @@ public class BotList { playerIO.save(bot); } else { bot.dropAll(true); - botsNameByWorldUuid.get(bot.level().uuid.toString()).remove(bot.getBukkitEntity().getRealName()); + botsNameByWorldUuid.getOrDefault(bot.level().uuid.toString(), new HashSet<>()).remove(bot.getBukkitEntity().getRealName()); } if (bot.isPassenger() && event.shouldSave()) { @@ -305,6 +306,17 @@ public class BotList { bots.forEach(this::loadNewBot); } + public void updateBotLevel(ServerBot bot, ServerLevel level) { + String prevUuid = bot.level().uuid.toString(); + String newUuid = level.uuid.toString(); + this.botsNameByWorldUuid + .computeIfAbsent(newUuid, (k) -> new HashSet<>()) + .add(bot.getBukkitEntity().getRealName()); + this.botsNameByWorldUuid + .computeIfAbsent(prevUuid, (k) -> new HashSet<>()) + .remove(bot.getBukkitEntity().getRealName()); + } + public void networkTick() { this.bots.forEach(ServerBot::networkTick); } diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bot/ServerBot.java b/leaves-server/src/main/java/org/leavesmc/leaves/bot/ServerBot.java index 516779c9..4eec9265 100644 --- a/leaves-server/src/main/java/org/leavesmc/leaves/bot/ServerBot.java +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bot/ServerBot.java @@ -275,6 +275,12 @@ public class ServerBot extends ServerPlayer { return this; } + @Override + public void setServerLevel(ServerLevel level) { + BotList.INSTANCE.updateBotLevel(this, level); + super.setServerLevel(level); + } + @Override public void knockback(double strength, double x, double z, @Nullable Entity attacker, EntityKnockbackEvent.@NotNull Cause eventCause) { if (!this.hurtMarked) { diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/replay/Recorder.java b/leaves-server/src/main/java/org/leavesmc/leaves/replay/Recorder.java index 8ab96394..ed711d6f 100644 --- a/leaves-server/src/main/java/org/leavesmc/leaves/replay/Recorder.java +++ b/leaves-server/src/main/java/org/leavesmc/leaves/replay/Recorder.java @@ -26,14 +26,17 @@ import net.minecraft.network.protocol.configuration.ClientboundUpdateEnabledFeat import net.minecraft.network.protocol.game.ClientboundAddEntityPacket; import net.minecraft.network.protocol.game.ClientboundGameEventPacket; import net.minecraft.network.protocol.game.ClientboundPlayerChatPacket; +import net.minecraft.network.protocol.game.ClientboundPlayerPositionPacket; import net.minecraft.network.protocol.game.ClientboundSetTimePacket; import net.minecraft.network.protocol.game.ClientboundSystemChatPacket; +import net.minecraft.network.protocol.game.ClientboundTrackedWaypointPacket; import net.minecraft.network.protocol.login.ClientboundLoginFinishedPacket; import net.minecraft.server.MinecraftServer; import net.minecraft.server.RegistryLayer; import net.minecraft.server.packs.repository.KnownPack; import net.minecraft.tags.TagNetworkSerialization; import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.PositionMoveRotation; import net.minecraft.world.flag.FeatureFlags; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -41,23 +44,27 @@ import org.leavesmc.leaves.LeavesLogger; import java.io.File; import java.io.IOException; +import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; public class Recorder extends Connection { - public static final Executor saveService = Executors.newVirtualThreadPerTaskExecutor(); public static final LeavesLogger LOGGER = LeavesLogger.LOGGER; + public final ExecutorService saveService = Executors.newSingleThreadExecutor(); private final ReplayFile replayFile; private final ServerPhotographer photographer; private final RecorderOption recorderOption; private final RecordMetaData metaData; + private final AtomicBoolean isSaving = new AtomicBoolean(false); private boolean stopped = false; private boolean paused = false; @@ -68,7 +75,6 @@ public class Recorder extends Connection { private long timeShift = 0; private boolean isSaved; - private boolean isSaving; private ConnectionProtocol state = ConnectionProtocol.LOGIN; public Recorder(ServerPhotographer photographer, RecorderOption recorderOption, File replayFile) throws IOException { @@ -77,7 +83,7 @@ public class Recorder extends Connection { this.photographer = photographer; this.recorderOption = recorderOption; this.metaData = new RecordMetaData(); - this.replayFile = new ReplayFile(replayFile); + this.replayFile = new ReplayFile(replayFile, saveService); this.channel = new LocalChannel(); } @@ -93,6 +99,8 @@ public class Recorder extends Connection { this.savePacket(new ClientboundLoginFinishedPacket(photographer.getGameProfile()), ConnectionProtocol.LOGIN); this.startConfiguration(); + savePacket(ClientboundPlayerPositionPacket.of(photographer.getId(), PositionMoveRotation.of(photographer), Collections.emptySet())); + if (recorderOption.forceWeather != null) { setWeather(recorderOption.forceWeather); } @@ -170,40 +178,46 @@ public class Recorder extends Connection { @Override public void send(@NotNull Packet packet, @Nullable ChannelFutureListener callbacks, boolean flush) { - if (!stopped) { - if (packet instanceof BundlePacket packet1) { + if (stopped) { + return; + } + switch (packet) { + case BundlePacket packet1 -> { packet1.subPackets().forEach(subPacket -> send(subPacket, null)); return; } - - if (packet instanceof ClientboundAddEntityPacket packet1) { + case ClientboundAddEntityPacket packet1 -> { if (packet1.getType() == EntityType.PLAYER) { metaData.players.add(packet1.getUUID()); saveMetadata(); } } - - if (packet instanceof ClientboundDisconnectPacket) { + case ClientboundDisconnectPacket ignored -> { return; } - - if (recorderOption.forceDayTime != -1 && packet instanceof ClientboundSetTimePacket packet1) { - packet = new ClientboundSetTimePacket(packet1.dayTime(), recorderOption.forceDayTime, false); - } - - if (recorderOption.forceWeather != null && packet instanceof ClientboundGameEventPacket packet1) { - ClientboundGameEventPacket.Type type = packet1.getEvent(); - if (type == ClientboundGameEventPacket.START_RAINING || type == ClientboundGameEventPacket.STOP_RAINING || type == ClientboundGameEventPacket.RAIN_LEVEL_CHANGE || type == ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE) { - return; - } - } - - if (recorderOption.ignoreChat && (packet instanceof ClientboundSystemChatPacket || packet instanceof ClientboundPlayerChatPacket)) { + case ClientboundTrackedWaypointPacket ignored -> { return; } - - savePacket(packet); + default -> { + } } + + if (recorderOption.forceDayTime != -1 && packet instanceof ClientboundSetTimePacket packet1) { + packet = new ClientboundSetTimePacket(packet1.dayTime(), recorderOption.forceDayTime, false); + } + + if (recorderOption.forceWeather != null && packet instanceof ClientboundGameEventPacket packet1) { + ClientboundGameEventPacket.Type type = packet1.getEvent(); + if (type == ClientboundGameEventPacket.START_RAINING || type == ClientboundGameEventPacket.STOP_RAINING || type == ClientboundGameEventPacket.RAIN_LEVEL_CHANGE || type == ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE) { + return; + } + } + + if (recorderOption.ignoreChat && (packet instanceof ClientboundSystemChatPacket || packet instanceof ClientboundPlayerChatPacket)) { + return; + } + + savePacket(packet); } private void saveMetadata() { @@ -235,36 +249,23 @@ public class Recorder extends Connection { } public CompletableFuture saveRecording(File dest, boolean save) { - isSaved = true; - if (!isSaving) { - isSaving = true; - metaData.duration = (int) lastPacket; - return CompletableFuture.runAsync(() -> { - saveMetadata(); - boolean interrupted = false; - try { - if (save) { - replayFile.closeAndSave(dest); - } else { - replayFile.closeNotSave(); - } - } catch (IOException e) { - e.printStackTrace(); - throw new CompletionException(e); - } finally { - if (interrupted) { - Thread.currentThread().interrupt(); - } - } - }, runnable -> { - final Thread thread = new Thread(runnable, "Recording file save thread"); - thread.start(); - }); - } else { + if (!isSaving.compareAndSet(false, true)) { LOGGER.warning("saveRecording() called twice"); - return CompletableFuture.supplyAsync(() -> { - throw new IllegalStateException("saveRecording() called twice"); - }); + return CompletableFuture.failedFuture(new IllegalStateException("saveRecording() called twice")); } + isSaved = true; + metaData.duration = (int) lastPacket; + return CompletableFuture.runAsync(() -> { + try { + replayFile.saveMetaData(metaData); + if (save) { + replayFile.closeAndSave(dest); + } else { + replayFile.closeNotSave(); + } + } catch (IOException e) { + throw new CompletionException(e); + } + }, saveService); } } diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/replay/ReplayFile.java b/leaves-server/src/main/java/org/leavesmc/leaves/replay/ReplayFile.java index 4c93b3f5..7d27c7da 100644 --- a/leaves-server/src/main/java/org/leavesmc/leaves/replay/ReplayFile.java +++ b/leaves-server/src/main/java/org/leavesmc/leaves/replay/ReplayFile.java @@ -34,12 +34,12 @@ import java.nio.file.Files; import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.concurrent.ExecutorService; import java.util.zip.CRC32; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import static org.leavesmc.leaves.replay.Recorder.LOGGER; -import static org.leavesmc.leaves.replay.Recorder.saveService; public class ReplayFile { @@ -59,8 +59,10 @@ public class ReplayFile { private final File metaFile; private final Map> protocols; + private final ExecutorService saveService; - public ReplayFile(@NotNull File name) throws IOException { + public ReplayFile(@NotNull File name, ExecutorService saveService) throws IOException { + this.saveService = saveService; this.tmpDir = new File(name.getParentFile(), name.getName() + ".tmp"); if (tmpDir.exists()) { if (!ReplayFile.deleteDir(tmpDir)) { diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/replay/ServerPhotographer.java b/leaves-server/src/main/java/org/leavesmc/leaves/replay/ServerPhotographer.java index bb45d241..a1845ecc 100644 --- a/leaves-server/src/main/java/org/leavesmc/leaves/replay/ServerPhotographer.java +++ b/leaves-server/src/main/java/org/leavesmc/leaves/replay/ServerPhotographer.java @@ -55,6 +55,8 @@ public class ServerPhotographer extends ServerPlayer { GameProfile profile = new GameProfile(UUID.randomUUID(), state.id); ServerPhotographer photographer = new ServerPhotographer(server, world, profile); + photographer.absSnapTo(state.loc.x(), state.loc.y(), state.loc.z(), state.loc.getYaw(), state.loc.getPitch()); + photographer.recorder = new Recorder(photographer, state.option, new File("replay", state.id)); photographer.saveFile = new File("replay", state.id + ".mcpr"); photographer.createState = state; @@ -146,6 +148,10 @@ public class ServerPhotographer extends ServerPlayer { this.followPlayer = followPlayer; } + public ServerPlayer getFollowPlayer() { + return followPlayer; + } + public void setSaveFile(File saveFile) { this.saveFile = saveFile; }