diff --git a/patches/unapplied/0006-Try-fixing-folia-273.patch b/patches/unapplied/0006-Try-fixing-folia-273.patch deleted file mode 100644 index 6cfcb87..0000000 --- a/patches/unapplied/0006-Try-fixing-folia-273.patch +++ /dev/null @@ -1,19 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: MrHua269 -Date: Mon, 12 Aug 2024 10:37:33 +0800 -Subject: [PATCH] Try fixing folia #273 - - -diff --git a/src/main/java/org/bukkit/inventory/CraftingRecipe.java b/src/main/java/org/bukkit/inventory/CraftingRecipe.java -index e8c3afda92d4ae5430d622ea18500985d6cc00f2..c78c1ee19a0960c6eca685a3b0d1ddbcb2831e94 100644 ---- a/src/main/java/org/bukkit/inventory/CraftingRecipe.java -+++ b/src/main/java/org/bukkit/inventory/CraftingRecipe.java -@@ -102,7 +102,7 @@ public abstract class CraftingRecipe implements Recipe, Keyed { - @ApiStatus.Internal - @NotNull - protected static ItemStack checkResult(@NotNull ItemStack result) { -- Preconditions.checkArgument(result.isEmpty(), "Recipe cannot have an empty result"); // Paper -+ Preconditions.checkArgument(!result.isEmpty(), "Recipe cannot have an empty result"); // Paper - return result; - } - } diff --git a/patches/unapplied/api/0002-Leaves-Replay-Mod-API.patch b/patches/unapplied/api/0002-Leaves-Replay-Mod-API.patch deleted file mode 100644 index 497b7e8..0000000 --- a/patches/unapplied/api/0002-Leaves-Replay-Mod-API.patch +++ /dev/null @@ -1,129 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: MrHua269 -Date: Sun, 17 Mar 2024 00:00:45 +0000 -Subject: [PATCH] Leaves Replay Mod API - - -diff --git a/src/main/java/org/bukkit/Bukkit.java b/src/main/java/org/bukkit/Bukkit.java -index b4327a55c422380ca6b3a1dc47c3adbe76de4655..8645a137c6beb5fe9bbb30159317a35a4cd96a50 100644 ---- a/src/main/java/org/bukkit/Bukkit.java -+++ b/src/main/java/org/bukkit/Bukkit.java -@@ -2918,4 +2918,10 @@ public final class Bukkit { - public static Server.Spigot spigot() { - return server.spigot(); - } -+ -+ // Leaves start - Photographer API -+ public static @NotNull top.leavesmc.leaves.entity.PhotographerManager getPhotographerManager() { -+ return server.getPhotographerManager(); -+ } -+ // Leaves end - Photographer API - } -diff --git a/src/main/java/org/bukkit/Server.java b/src/main/java/org/bukkit/Server.java -index 2a888d33eff4487f23463c565c9f75c40fba7d94..3a78bcb6f681b44594c4f5f35120b67adbe6aeb5 100644 ---- a/src/main/java/org/bukkit/Server.java -+++ b/src/main/java/org/bukkit/Server.java -@@ -2546,4 +2546,8 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi - */ - public boolean isGlobalTickThread(); - // Folia end - region threading API -+ -+ // Leaves start - Photographer API -+ @NotNull top.leavesmc.leaves.entity.PhotographerManager getPhotographerManager(); -+ // Leaves end - Photographer API - } -diff --git a/src/main/java/top/leavesmc/leaves/entity/Photographer.java b/src/main/java/top/leavesmc/leaves/entity/Photographer.java -new file mode 100644 -index 0000000000000000000000000000000000000000..bfa6fe2a0ca095170aa5e073251402cf83a53ba0 ---- /dev/null -+++ b/src/main/java/top/leavesmc/leaves/entity/Photographer.java -@@ -0,0 +1,27 @@ -+package top.leavesmc.leaves.entity; -+ -+import org.bukkit.entity.Player; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+ -+import java.io.File; -+ -+public interface Photographer extends Player { -+ -+ @NotNull -+ public String getId(); -+ -+ public void setRecordFile(@NotNull File file); -+ -+ public void stopRecording(); -+ -+ public void stopRecording(boolean async); -+ -+ public void stopRecording(boolean async, boolean save); -+ -+ public void pauseRecording(); -+ -+ public void resumeRecording(); -+ -+ public void setFollowPlayer(@Nullable Player player); -+} -diff --git a/src/main/java/top/leavesmc/leaves/entity/PhotographerManager.java b/src/main/java/top/leavesmc/leaves/entity/PhotographerManager.java -new file mode 100644 -index 0000000000000000000000000000000000000000..2889a4835edea4254a3d35fc7861983644a1dc4b ---- /dev/null -+++ b/src/main/java/top/leavesmc/leaves/entity/PhotographerManager.java -@@ -0,0 +1,32 @@ -+package top.leavesmc.leaves.entity; -+ -+import org.bukkit.Location; -+import org.bukkit.util.Consumer; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import top.leavesmc.leaves.replay.BukkitRecorderOption; -+ -+import java.util.Collection; -+import java.util.UUID; -+ -+public interface PhotographerManager { -+ @Nullable -+ public Photographer getPhotographer(@NotNull UUID uuid); -+ -+ @Nullable -+ public Photographer getPhotographer(@NotNull String id); -+ -+ @Nullable -+ public Photographer createPhotographer(@NotNull String id, @NotNull Location location); -+ -+ @Nullable -+ public Photographer createPhotographer(@NotNull String id, @NotNull Location location, @NotNull BukkitRecorderOption recorderOption); -+ -+ public void removePhotographer(@NotNull String id); -+ -+ public void removePhotographer(@NotNull UUID uuid); -+ -+ public void removeAllPhotographers(); -+ -+ public Collection getPhotographers(); -+} -diff --git a/src/main/java/top/leavesmc/leaves/replay/BukkitRecorderOption.java b/src/main/java/top/leavesmc/leaves/replay/BukkitRecorderOption.java -new file mode 100644 -index 0000000000000000000000000000000000000000..3df4a6055b91c28e273d6fb2697b608778f40a9c ---- /dev/null -+++ b/src/main/java/top/leavesmc/leaves/replay/BukkitRecorderOption.java -@@ -0,0 +1,18 @@ -+package top.leavesmc.leaves.replay; -+ -+public class BukkitRecorderOption { -+ -+ // public int recordDistance = -1; -+ public String serverName = "Leaves"; -+ public BukkitRecordWeather forceWeather = BukkitRecordWeather.NULL; -+ public int forceDayTime = -1; -+ public boolean ignoreChat = false; -+ // public boolean ignoreItem = false; -+ -+ public enum BukkitRecordWeather { -+ CLEAR, -+ RAIN, -+ THUNDER, -+ NULL -+ } -+} diff --git a/patches/unapplied/server/0052-Leaves-Replay-Mod-API.patch b/patches/unapplied/server/0052-Leaves-Replay-Mod-API.patch deleted file mode 100644 index aa12e8e..0000000 --- a/patches/unapplied/server/0052-Leaves-Replay-Mod-API.patch +++ /dev/null @@ -1,1687 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: MrHua269 -Date: Sun, 17 Mar 2024 02:13:30 +0000 -Subject: [PATCH] Leaves Replay Mod API - - -diff --git a/src/main/java/io/papermc/paper/util/TickThread.java b/src/main/java/io/papermc/paper/util/TickThread.java -index 906f1c9e619a924c622acc76652a4569305edc8d..620e12a38c67230ad8591a4a32bcb932426356cc 100644 ---- a/src/main/java/io/papermc/paper/util/TickThread.java -+++ b/src/main/java/io/papermc/paper/util/TickThread.java -@@ -258,7 +258,7 @@ public class TickThread extends Thread { - return true; - } - -- if (entity instanceof ServerPlayer serverPlayer) { -+ if ((entity instanceof ServerPlayer serverPlayer) && !(entity instanceof top.leavesmc.leaves.replay.ServerPhotographer)) { //Leaves - ReplayMod API - ServerGamePacketListenerImpl conn = serverPlayer.connection; - return conn != null && worldData.connections.contains(conn.connection); - } else { -diff --git a/src/main/java/me/earthme/luminol/utils/NullStatsCounter.java b/src/main/java/me/earthme/luminol/utils/NullStatsCounter.java -new file mode 100644 -index 0000000000000000000000000000000000000000..2b1354bb249bfc373a25b25e5a9c05f012e7edee ---- /dev/null -+++ b/src/main/java/me/earthme/luminol/utils/NullStatsCounter.java -@@ -0,0 +1,37 @@ -+package me.earthme.luminol.utils; -+ -+import com.mojang.datafixers.DataFixer; -+import net.minecraft.server.MinecraftServer; -+import net.minecraft.stats.ServerStatsCounter; -+import net.minecraft.stats.Stat; -+import net.minecraft.world.entity.player.Player; -+ -+import java.io.File; -+ -+public class NullStatsCounter extends ServerStatsCounter { -+ private static final File UNKOWN_FILE = new File("NULL_STATS_REMOVE_THIS"); -+ -+ public NullStatsCounter(MinecraftServer server) { -+ super(server, UNKOWN_FILE); -+ } -+ -+ @Override -+ public void save() { -+ -+ } -+ -+ @Override -+ public void setValue(Player player, Stat stat, int value) { -+ -+ } -+ -+ @Override -+ public void parseLocal(DataFixer dataFixer, String json) { -+ -+ } -+ -+ @Override -+ public int getValue(Stat stat) { -+ return 0; -+ } -+} -diff --git a/src/main/java/net/minecraft/commands/arguments/EntityArgument.java b/src/main/java/net/minecraft/commands/arguments/EntityArgument.java -index 8d79cfa371546996ef65f94232c1d344e7c590ec..9c262c82d9ab24bdbbe03df8cee3d5d99e8f8868 100644 ---- a/src/main/java/net/minecraft/commands/arguments/EntityArgument.java -+++ b/src/main/java/net/minecraft/commands/arguments/EntityArgument.java -@@ -147,6 +147,7 @@ public class EntityArgument implements ArgumentType { - if (icompletionprovider instanceof CommandSourceStack commandSourceStack && commandSourceStack.getEntity() instanceof ServerPlayer sourcePlayer) { - collection = new java.util.ArrayList<>(); - for (final ServerPlayer player : commandSourceStack.getServer().getPlayerList().getPlayers()) { -+ if (player instanceof top.leavesmc.leaves.replay.ServerPhotographer) continue; // Leaves - skip photographer - if (sourcePlayer.getBukkitEntity().canSee(player.getBukkitEntity())) { - collection.add(player.getGameProfile().getName()); - } -diff --git a/src/main/java/net/minecraft/commands/arguments/selector/EntitySelector.java b/src/main/java/net/minecraft/commands/arguments/selector/EntitySelector.java -index 676a1499747b071515479130875157263d3a8352..e5ef298dc1df9cc42b3d349939a966b77fc0d554 100644 ---- a/src/main/java/net/minecraft/commands/arguments/selector/EntitySelector.java -+++ b/src/main/java/net/minecraft/commands/arguments/selector/EntitySelector.java -@@ -122,6 +122,7 @@ public class EntitySelector { - return this.findPlayers(source); - } else if (this.playerName != null) { - ServerPlayer entityplayer = source.getServer().getPlayerList().getPlayerByName(this.playerName); -+ entityplayer = entityplayer instanceof top.leavesmc.leaves.replay.ServerPhotographer ? null : entityplayer; // Leaves - skip photographer - - return (List) (entityplayer == null ? Collections.emptyList() : Lists.newArrayList(new ServerPlayer[]{entityplayer})); - } else if (this.entityUUID != null) { -@@ -137,6 +138,7 @@ public class EntitySelector { - ServerLevel worldserver = (ServerLevel) iterator.next(); - - entity = worldserver.getEntity(this.entityUUID); -+ entity = entity instanceof top.leavesmc.leaves.replay.ServerPhotographer ? null : entity; // Leaves - skip photographer - } while (entity == null); - - return Lists.newArrayList(new Entity[]{entity}); -@@ -145,7 +147,7 @@ public class EntitySelector { - Predicate predicate = this.getPredicate(vec3d); - - if (this.currentEntity) { -- return (List) (source.getEntity() != null && predicate.test(source.getEntity()) ? Lists.newArrayList(new Entity[]{source.getEntity()}) : Collections.emptyList()); -+ return (List) (source.getEntity() != null && !(source.getEntity() instanceof top.leavesmc.leaves.replay.ServerPhotographer) && predicate.test(source.getEntity()) ? Lists.newArrayList(new Entity[]{source.getEntity()}) : Collections.emptyList()); // Leaves - skip photographer - } else { - List list = Lists.newArrayList(); - -@@ -160,6 +162,7 @@ public class EntitySelector { - this.addEntities(list, worldserver1, vec3d, predicate); - } - } -+ list.removeIf(entity -> entity instanceof top.leavesmc.leaves.replay.ServerPhotographer); // Leaves - skip photographer - - return this.sortAndLimit(vec3d, list); - } -@@ -200,9 +203,11 @@ public class EntitySelector { - - if (this.playerName != null) { - entityplayer = source.getServer().getPlayerList().getPlayerByName(this.playerName); -+ entityplayer = entityplayer instanceof top.leavesmc.leaves.replay.ServerPhotographer ? null : entityplayer; // Leaves - skip photographer - return (List) (entityplayer == null ? Collections.emptyList() : Lists.newArrayList(new ServerPlayer[]{entityplayer})); - } else if (this.entityUUID != null) { - entityplayer = source.getServer().getPlayerList().getPlayer(this.entityUUID); -+ entityplayer = entityplayer instanceof top.leavesmc.leaves.replay.ServerPhotographer ? null : entityplayer; // Leaves - skip photographer - return (List) (entityplayer == null ? Collections.emptyList() : Lists.newArrayList(new ServerPlayer[]{entityplayer})); - } else { - Vec3 vec3d = (Vec3) this.position.apply(source.getPosition()); -@@ -214,7 +219,7 @@ public class EntitySelector { - if (entity instanceof ServerPlayer) { - ServerPlayer entityplayer1 = (ServerPlayer) entity; - -- if (predicate.test(entityplayer1)) { -+ if (predicate.test(entityplayer1) && !(entityplayer1 instanceof top.leavesmc.leaves.replay.ServerPhotographer)) { // Leaves - skip photographer - return Lists.newArrayList(new ServerPlayer[]{entityplayer1}); - } - } -@@ -225,7 +230,7 @@ public class EntitySelector { - Object object; - - if (this.isWorldLimited()) { -- object = source.getLevel().getPlayers(predicate, i); -+ object = source.getLevel().getPlayers((entityplayer3 -> !(entityplayer3 instanceof top.leavesmc.leaves.replay.ServerPhotographer) && predicate.test(entityplayer3)), i); // Leaves - skip photographer - } else { - object = Lists.newArrayList(); - Iterator iterator = source.getServer().getPlayerList().getPlayers().iterator(); -@@ -233,7 +238,7 @@ public class EntitySelector { - while (iterator.hasNext()) { - ServerPlayer entityplayer2 = (ServerPlayer) iterator.next(); - -- if (predicate.test(entityplayer2)) { -+ if (predicate.test(entityplayer2) && !(entityplayer2 instanceof top.leavesmc.leaves.replay.ServerPhotographer)) { // Leaves - skip photographer - ((List) object).add(entityplayer2); - if (((List) object).size() >= i) { - return (List) object; -diff --git a/src/main/java/net/minecraft/network/Connection.java b/src/main/java/net/minecraft/network/Connection.java -index 936f8663b23908ac5de2076401a5d508f88a0376..0e8e57c291539e50f61e8178fc355fa698248967 100644 ---- a/src/main/java/net/minecraft/network/Connection.java -+++ b/src/main/java/net/minecraft/network/Connection.java -@@ -179,6 +179,14 @@ public class Connection extends SimpleChannelInboundHandler> { - } - // Folia end - region threading - -+ // Leaves start - fakeplayer -+ public void setListenerForce(PacketListener packetListener) { -+ Validate.notNull(packetListener, "packetListener"); -+ this.packetListener = packetListener; -+ this.disconnectListener = null; -+ } -+ // Leaves end - fakeplayer -+ - public void channelActive(ChannelHandlerContext channelhandlercontext) throws Exception { - super.channelActive(channelhandlercontext); - this.channel = channelhandlercontext.channel(); -diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index 08b0d3b970a7c86f577d20487d5fe3930b8eae6e..8ea86315e09a6b182bab03ee902ef92c5e18d962 100644 ---- a/src/main/java/net/minecraft/server/MinecraftServer.java -+++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -1727,7 +1727,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop list = new java.util.ArrayList<>(this.playerList.getPlayers()); // Folia - region threading -+ List list = new java.util.ArrayList<>(this.playerList.realPlayers); // Folia - region threading // Leaves - only real player - int i = this.getMaxPlayers(); - - if (this.hidesOnlinePlayers()) { -diff --git a/src/main/java/net/minecraft/server/PlayerAdvancements.java b/src/main/java/net/minecraft/server/PlayerAdvancements.java -index 24e5993b281448734eb67c7a8439a349bbf9fd72..da840e40dbb0f0c8dab5fbb27e2cfe6a25aae498 100644 ---- a/src/main/java/net/minecraft/server/PlayerAdvancements.java -+++ b/src/main/java/net/minecraft/server/PlayerAdvancements.java -@@ -221,6 +221,11 @@ public class PlayerAdvancements { - } - - public boolean award(AdvancementHolder advancement, String criterionName) { -+ // Leaves start - photographer can't get advancement -+ if (player instanceof top.leavesmc.leaves.replay.ServerPhotographer) { // Leaves - and photographer -+ return false; -+ } -+ // Leaves end - photographer can't get advancement - boolean flag = false; - AdvancementProgress advancementprogress = this.getOrStartProgress(advancement); - boolean flag1 = advancementprogress.isDone(); -diff --git a/src/main/java/net/minecraft/server/commands/OpCommand.java b/src/main/java/net/minecraft/server/commands/OpCommand.java -index e7b444a10b244828827b3c66c53465206ea8e0ec..030601fdfde2232a933b2ad7022e9909db3a3783 100644 ---- a/src/main/java/net/minecraft/server/commands/OpCommand.java -+++ b/src/main/java/net/minecraft/server/commands/OpCommand.java -@@ -25,7 +25,7 @@ public class OpCommand { - (context, builder) -> { - PlayerList playerList = context.getSource().getServer().getPlayerList(); - return SharedSuggestionProvider.suggest( -- playerList.getPlayers() -+ playerList.realPlayers // Leaves - only real player - .stream() - .filter(player -> !playerList.isOp(player.getGameProfile())) - .map(player -> player.getGameProfile().getName()), -diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java -index 0086a1a4611b983eefd0cb7bf8e9cff677246d0f..dcbf18eb7fb9a37fcd7faf3efe023d2503b4091a 100644 ---- a/src/main/java/net/minecraft/server/level/ServerPlayer.java -+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java -@@ -197,7 +197,7 @@ public class ServerPlayer extends Player { - private static final int FLY_STAT_RECORDING_SPEED = 25; - public ServerGamePacketListenerImpl connection; - public final MinecraftServer server; -- public final ServerPlayerGameMode gameMode; -+ public ServerPlayerGameMode gameMode; - private final PlayerAdvancements advancements; - private final ServerStatsCounter stats; - private float lastRecordedHealthAndAbsorption = Float.MIN_VALUE; -diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -index e0f12426f18b343b27089440cd01127d79600ef0..fbe2372d871f8f2b53b04f7bddf3a5652f2eb394 100644 ---- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -+++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -@@ -297,7 +297,13 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - public ServerGamePacketListenerImpl(MinecraftServer server, Connection connection, ServerPlayer player, CommonListenerCookie clientData) { - super(server, connection, clientData, player); // CraftBukkit - this.chunkSender = new PlayerChunkSender(connection.isMemoryConnection()); -- connection.setListener(this); -+ //Leaves start - ReplayMod API -+ if(player instanceof top.leavesmc.leaves.replay.ServerPhotographer){ -+ connection.setListenerForce(this); -+ }else{ -+ connection.setListener(this); -+ } -+ //Leaves end - this.player = player; - player.connection = this; - player.getTextFilter().join(); -diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java -index 90be312057221a5a78066d89783c5e22008d797d..24f52fde099bac6f7c6a61cfc28a2b23437a116d 100644 ---- a/src/main/java/net/minecraft/server/players/PlayerList.java -+++ b/src/main/java/net/minecraft/server/players/PlayerList.java -@@ -13,15 +13,7 @@ import java.net.SocketAddress; - import java.nio.file.Path; - import java.text.SimpleDateFormat; - import java.time.Instant; --import java.util.Collection; --import java.util.EnumSet; --import java.util.Iterator; --import java.util.List; --import java.util.Map; --import java.util.Objects; --import java.util.Optional; --import java.util.Set; --import java.util.UUID; -+import java.util.*; - import java.util.function.Function; - import java.util.function.Predicate; - import javax.annotation.Nullable; -@@ -121,6 +113,11 @@ import org.bukkit.event.player.PlayerRespawnEvent; - import org.bukkit.event.player.PlayerRespawnEvent.RespawnReason; - import org.bukkit.event.player.PlayerSpawnChangeEvent; - // CraftBukkit end -+import top.leavesmc.leaves.replay.ServerPhotographer; -+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; -+import java.util.concurrent.CompletableFuture; -+import io.papermc.paper.threadedregions.RegionizedServer; -+import io.papermc.paper.util.TickThread; - - public abstract class PlayerList { - -@@ -153,6 +150,7 @@ public abstract class PlayerList { - private boolean allowCheatsForAllPlayers; - private static final boolean ALLOW_LOGOUTIVATOR = false; - private int sendAllPlayerInfoIn; -+ public final List realPlayers = new java.util.concurrent.CopyOnWriteArrayList(); // Leaves - replay api - - // CraftBukkit start - private CraftServer cserver; -@@ -244,6 +242,121 @@ public abstract class PlayerList { - } - abstract public void loadAndSaveFiles(); // Paper - fix converting txt to json file; moved from DedicatedPlayerList constructor - -+ // Leaves start - replay api -+ public CompletableFuture placeNewPhotographer(Connection connection, ServerPhotographer player, ServerLevel worldserver, Location location) { -+ Runnable scheduledJoin = () -> { -+ player.isRealPlayer = true; // Paper -+ player.loginTime = System.currentTimeMillis(); // Paper -+ -+ ServerLevel worldserver1 = worldserver; -+ -+ player.setServerLevel(worldserver1); -+ player.spawnIn(worldserver1); -+ player.gameMode.setLevel((ServerLevel) player.level()); -+ -+ LevelData worlddata = worldserver1.getLevelData(); -+ -+ player.loadGameTypes(null); -+ ServerGamePacketListenerImpl playerconnection = new ServerGamePacketListenerImpl(this.server, connection, player, CommonListenerCookie.createInitial(player.gameProfile)); -+ //worldserver1.getCurrentWorldData().connections.add(connection); -+ GameRules gamerules = worldserver1.getGameRules(); -+ boolean flag = gamerules.getBoolean(GameRules.RULE_DO_IMMEDIATE_RESPAWN); -+ boolean flag1 = gamerules.getBoolean(GameRules.RULE_REDUCEDDEBUGINFO); -+ boolean flag2 = gamerules.getBoolean(GameRules.RULE_LIMITED_CRAFTING); -+ -+ playerconnection.send(new ClientboundLoginPacket(player.getId(), worlddata.isHardcore(), this.server.levelKeys(), this.getMaxPlayers(), worldserver1.getWorld().getSendViewDistance(), worldserver1.getWorld().getSimulationDistance(), flag1, !flag, flag2, player.createCommonSpawnInfo(worldserver1))); // Paper - replace old player chunk management -+ player.getBukkitEntity().sendSupportedChannels(); // CraftBukkit -+ playerconnection.send(new ClientboundChangeDifficultyPacket(worlddata.getDifficulty(), worlddata.isDifficultyLocked())); -+ playerconnection.send(new ClientboundPlayerAbilitiesPacket(player.getAbilities())); -+ playerconnection.send(new ClientboundSetCarriedItemPacket(player.getInventory().selected)); -+ playerconnection.send(new ClientboundUpdateRecipesPacket(this.server.getRecipeManager().getRecipes())); -+ this.sendPlayerPermissionLevel(player); -+ player.getStats().markAllDirty(); -+ player.getRecipeBook().sendInitialRecipeBook(player); -+ -+ this.server.invalidateStatus(); -+ -+ playerconnection.teleport(player.getX(), player.getY(), player.getZ(), player.getYRot(), player.getXRot()); -+ ServerStatus serverping = this.server.getStatus(); -+ -+ if (serverping != null) { -+ player.sendServerStatus(serverping); -+ } -+ -+ this.players.add(player); -+ this.playersByName.put(player.getScoreboardName().toLowerCase(java.util.Locale.ROOT), player); // Spigot -+ this.playersByUUID.put(player.getUUID(), player); -+ -+ player.supressTrackerForLogin = true; -+ worldserver1.addNewPlayer(player); -+ this.server.getCustomBossEvents().onPlayerConnect(player); -+ mountSavedVehicle(player, worldserver1, null); -+ CraftPlayer bukkitPlayer = player.getBukkitEntity(); -+ -+ player.containerMenu.transferTo(player.containerMenu, bukkitPlayer); -+ if (!player.connection.isAcceptingMessages()) { -+ return; -+ } -+ -+ // top.leavesmc.leaves.protocol.core.LeavesProtocolManager.handlePlayerJoin(player); // Leaves - protocol -+ -+ final List playersCopy = new ArrayList<>(this.players); -+ final List onlinePlayers = Lists.newArrayListWithExpectedSize(playersCopy.size() - 1); -+ for (ServerPlayer entityplayer1 : playersCopy) { -+ if (entityplayer1 == player || !bukkitPlayer.canSee(entityplayer1.getBukkitEntity())) { -+ continue; -+ } -+ -+ onlinePlayers.add(entityplayer1); -+ } -+ -+ if (!onlinePlayers.isEmpty()) { -+ player.connection.send(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(onlinePlayers, player)); -+ } -+ -+ player.sentListPacket = true; -+ player.supressTrackerForLogin = false; -+ ((ServerLevel)player.level()).getChunkSource().chunkMap.addEntity(player); -+ -+ this.sendLevelInfo(player, worldserver1); -+ -+ if (player.level() == worldserver1 && !worldserver1.players().contains(player)) { -+ worldserver1.addNewPlayer(player); -+ this.server.getCustomBossEvents().onPlayerConnect(player); -+ } -+ -+ worldserver1 = player.serverLevel(); -+ for (MobEffectInstance mobeffect : player.getActiveEffects()) { -+ playerconnection.send(new ClientboundUpdateMobEffectPacket(player.getId(), mobeffect)); -+ } -+ -+ if (player.isDeadOrDying()) { -+ net.minecraft.core.Holder plains = worldserver1.registryAccess().registryOrThrow(net.minecraft.core.registries.Registries.BIOME) -+ .getHolderOrThrow(net.minecraft.world.level.biome.Biomes.PLAINS); -+ player.connection.send(new net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket( -+ new net.minecraft.world.level.chunk.EmptyLevelChunk(worldserver1, player.chunkPosition(), plains), -+ worldserver1.getLightEngine(), (java.util.BitSet)null, (java.util.BitSet) null, false) -+ ); -+ } -+ }; -+ -+ final BlockPos pos = io.papermc.paper.util.MCUtil.toBlockPosition(location); -+ if (TickThread.isTickThreadFor(worldserver,pos)){ -+ scheduledJoin.run(); -+ return CompletableFuture.completedFuture(null); -+ } -+ -+ return CompletableFuture.runAsync(scheduledJoin,task -> -+ RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( -+ worldserver, -+ pos.getX() >> 4, -+ pos.getZ() >> 4, -+ task -+ ) -+ ); -+ } -+ // Leaves end - replay api -+ - public void loadSpawnForNewPlayer(final Connection connection, final ServerPlayer player, final CommonListenerCookie clientData, org.apache.commons.lang3.mutable.MutableObject data, org.apache.commons.lang3.mutable.MutableObject lastKnownName, ca.spottedleaf.concurrentutil.completable.Completable toComplete) { // Folia - region threading - rewrite login process - player.isRealPlayer = true; // Paper - player.loginTime = System.currentTimeMillis(); // Paper - Replace OfflinePlayer#getLastPlayed -@@ -418,6 +531,7 @@ public abstract class PlayerList { - - // entityplayer.connection.send(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(this.players)); // CraftBukkit - replaced with loop below - this.players.add(player); -+ this.realPlayers.add(player); // Leaves - replay api - this.playersByName.put(player.getScoreboardName().toLowerCase(java.util.Locale.ROOT), player); // Spigot - this.playersByUUID.put(player.getUUID(), player); - // this.broadcastAll(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(entityplayer))); // CraftBukkit - replaced with loop below -@@ -471,6 +585,12 @@ public abstract class PlayerList { - continue; - } - -+ // Leaves start - skip photographer -+ if (entityplayer1 instanceof ServerPhotographer) { -+ continue; -+ } -+ // Leaves end - skip photographer -+ - onlinePlayers.add(entityplayer1); // Paper - Use single player info update packet on join - } - // Paper start - Use single player info update packet on join -@@ -691,6 +811,54 @@ public abstract class PlayerList { - - } - -+ // Leaevs start - replay mod api -+ public CompletableFuture removePhotographer(ServerPhotographer entityplayer) { -+ ServerLevel worldserver = entityplayer.serverLevel(); -+ -+ Runnable scheduledRemove = () -> { //Folia support -+ entityplayer.awardStat(Stats.LEAVE_GAME); -+ -+ if (entityplayer.containerMenu != entityplayer.inventoryMenu) { -+ entityplayer.closeContainer(org.bukkit.event.inventory.InventoryCloseEvent.Reason.DISCONNECT); -+ } -+ -+ if (TickThread.isTickThreadFor(entityplayer)) entityplayer.doTick(); -+ -+ if (false && this.collideRuleTeamName != null) { //We are running as folia, so the scoreboard is unavailable now -+ final net.minecraft.world.scores.Scoreboard scoreBoard = this.server.getLevel(Level.OVERWORLD).getScoreboard(); -+ final PlayerTeam team = scoreBoard.getPlayersTeam(this.collideRuleTeamName); -+ if (entityplayer.getTeam() == team && team != null) { -+ scoreBoard.removePlayerFromTeam(entityplayer.getScoreboardName(), team); -+ } -+ } -+ -+ //worldserver.getCurrentWorldData().connections.remove(entityplayer.recorder); -+ worldserver.removePlayerImmediately(entityplayer, Entity.RemovalReason.UNLOADED_WITH_PLAYER); -+ entityplayer.retireScheduler(); -+ entityplayer.getAdvancements().stopListening(); -+ this.players.remove(entityplayer); -+ this.playersByName.remove(entityplayer.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); -+ this.server.getCustomBossEvents().onPlayerDisconnect(entityplayer); -+ UUID uuid = entityplayer.getUUID(); -+ ServerPlayer entityplayer1 = this.playersByUUID.get(uuid); -+ -+ if (entityplayer1 == entityplayer) { -+ this.playersByUUID.remove(uuid); -+ } -+ -+ this.cserver.getScoreboardManager().removePlayer(entityplayer.getBukkitEntity()); -+ }; -+ -+ if (TickThread.isTickThreadFor(entityplayer)){ //Check if we are running on the target tick region -+ scheduledRemove.run(); -+ return CompletableFuture.completedFuture(null); -+ } -+ -+ //If not -+ return CompletableFuture.runAsync(scheduledRemove,task -> RegionizedServer.getInstance().taskQueue.queueTickTaskQueue(worldserver,(int)entityplayer.position.x >> 4,(int)entityplayer.position.z >> 4,task, PrioritisedExecutor.Priority.HIGHER)); -+ } -+ // Leaves stop - replay mod api -+ - public net.kyori.adventure.text.Component remove(ServerPlayer entityplayer) { // CraftBukkit - return string // Paper - return Component - // Paper start - Fix kick event leave message not being sent - return this.remove(entityplayer, net.kyori.adventure.text.Component.translatable("multiplayer.player.left", net.kyori.adventure.text.format.NamedTextColor.YELLOW, io.papermc.paper.configuration.GlobalConfiguration.get().messages.useDisplayNameInQuitMessage ? entityplayer.getBukkitEntity().displayName() : io.papermc.paper.adventure.PaperAdventure.asAdventure(entityplayer.getDisplayName()))); -@@ -758,6 +926,7 @@ public abstract class PlayerList { - // Folia - region threading - move to onDisconnect of common packet listener - entityplayer.getAdvancements().stopListening(); - this.players.remove(entityplayer); -+ this.realPlayers.remove(entityplayer); // Leaves - replay api - this.playersByName.remove(entityplayer.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); // Spigot - this.server.getCustomBossEvents().onPlayerDisconnect(entityplayer); - UUID uuid = entityplayer.getUUID(); -diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java -index c70eb23d9745bdbfcc340bb554cf0bf2db71f5de..469e175dc87e66ce73efcc04a68b19140cb7194f 100644 ---- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java -@@ -308,6 +308,7 @@ public final class CraftServer implements Server { - private final io.papermc.paper.logging.SysoutCatcher sysoutCatcher = new io.papermc.paper.logging.SysoutCatcher(); // Paper - private final CraftPotionBrewer potionBrewer = new CraftPotionBrewer(); // Paper - Custom Potion Mixes - -+ private final top.leavesmc.leaves.entity.CraftPhotographerManager photographerManager = new top.leavesmc.leaves.entity.CraftPhotographerManager(); //Leaves - ReplayMod API - // Paper start - Folia region threading API - private final io.papermc.paper.threadedregions.scheduler.FoliaRegionScheduler regionizedScheduler = new io.papermc.paper.threadedregions.scheduler.FoliaRegionScheduler(); // Folia - region threading - private final io.papermc.paper.threadedregions.scheduler.FoliaAsyncScheduler asyncScheduler = new io.papermc.paper.threadedregions.scheduler.FoliaAsyncScheduler(); -@@ -394,7 +395,7 @@ public final class CraftServer implements Server { - public CraftServer(DedicatedServer console, PlayerList playerList) { - this.console = console; - this.playerList = (DedicatedPlayerList) playerList; -- this.playerView = Collections.unmodifiableList(Lists.transform(playerList.players, new Function() { -+ this.playerView = Collections.unmodifiableList(Lists.transform(playerList.realPlayers, new Function() { // Leaves - replay api - @Override - public CraftPlayer apply(ServerPlayer player) { - return player.getBukkitEntity(); -@@ -3304,4 +3305,11 @@ public final class CraftServer implements Server { - } - - // Paper end -+ -+ // Leaves start - replay mod api -+ @Override -+ public top.leavesmc.leaves.entity.CraftPhotographerManager getPhotographerManager() { -+ return photographerManager; -+ } -+ // Leaves end - replay mod api - } -diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java -index 8c7e01972888df4ccbaccc4eebceeeb5ab357f4c..99956f506d543c2917746d5eb095598135251320 100644 ---- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java -+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java -@@ -57,6 +57,8 @@ import org.bukkit.util.NumberConversions; - import org.bukkit.util.Vector; - - import net.md_5.bungee.api.chat.BaseComponent; // Spigot -+import top.leavesmc.leaves.entity.CraftPhotographer; -+import top.leavesmc.leaves.replay.ServerPhotographer; - - public abstract class CraftEntity implements org.bukkit.entity.Entity { - private static PermissibleBase perm; -@@ -92,6 +94,8 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { - return new CraftHumanEntity(server, (net.minecraft.world.entity.player.Player) entity); - } - -+ if (entity instanceof ServerPhotographer) { return new CraftPhotographer(server, (ServerPhotographer) entity); } -+ - // Special case complex part, since there is no extra entity type for them - if (entity instanceof EnderDragonPart complexPart) { - if (complexPart.parentMob instanceof EnderDragon) { -diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -index a68485e29466f26d1cb94cd827f5a5d9645b1e5c..2a5e76b1cee25b05189bb75daad4ced939bbb343 100644 ---- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -@@ -2143,7 +2143,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player { - - @Override - public boolean canSee(Player player) { -- return this.canSee((org.bukkit.entity.Entity) player); -+ return !(player instanceof top.leavesmc.leaves.entity.Photographer) && this.canSee((org.bukkit.entity.Entity) player); // Leaves - skip photographer - } - - @Override -diff --git a/src/main/java/top/leavesmc/leaves/entity/CraftPhotographer.java b/src/main/java/top/leavesmc/leaves/entity/CraftPhotographer.java -new file mode 100644 -index 0000000000000000000000000000000000000000..4f58b6623a6b5c726d718ced6ab106af3e665e35 ---- /dev/null -+++ b/src/main/java/top/leavesmc/leaves/entity/CraftPhotographer.java -@@ -0,0 +1,73 @@ -+package top.leavesmc.leaves.entity; -+ -+import net.minecraft.server.level.ServerPlayer; -+import org.bukkit.craftbukkit.CraftServer; -+import org.bukkit.craftbukkit.entity.CraftPlayer; -+import org.bukkit.entity.Player; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import top.leavesmc.leaves.replay.ServerPhotographer; -+ -+import java.io.File; -+ -+public class CraftPhotographer extends CraftPlayer implements Photographer { -+ -+ public CraftPhotographer(CraftServer server, ServerPhotographer entity) { -+ super(server, entity); -+ } -+ -+ @Override -+ public void stopRecording() { -+ this.stopRecording(true); -+ } -+ -+ @Override -+ public void stopRecording(boolean async) { -+ this.stopRecording(async, true); -+ } -+ -+ @Override -+ public void stopRecording(boolean async, boolean save) { -+ this.getHandle().remove(async, save); -+ } -+ -+ @Override -+ public void pauseRecording() { -+ this.getHandle().pauseRecording(); -+ } -+ -+ @Override -+ public void resumeRecording() { -+ this.getHandle().resumeRecording(); -+ } -+ -+ @Override -+ public void setRecordFile(@NotNull File file) { -+ this.getHandle().setSaveFile(file); -+ } -+ -+ @Override -+ public void setFollowPlayer(@Nullable Player player) { -+ ServerPlayer serverPlayer = player != null ? ((CraftPlayer) player).getHandle() : null; -+ this.getHandle().setFollowPlayer(serverPlayer); -+ } -+ -+ @Override -+ public @NotNull String getId() { -+ return this.getHandle().createState.id; -+ } -+ -+ @Override -+ public ServerPhotographer getHandle() { -+ return (ServerPhotographer) entity; -+ } -+ -+ public void setHandle(final ServerPhotographer entity) { -+ super.setHandle(entity); -+ } -+ -+ @Override -+ public String toString() { -+ return "CraftPhotographer{" + "name=" + getName() + '}'; -+ } -+} -diff --git a/src/main/java/top/leavesmc/leaves/entity/CraftPhotographerManager.java b/src/main/java/top/leavesmc/leaves/entity/CraftPhotographerManager.java -new file mode 100644 -index 0000000000000000000000000000000000000000..5effb1022bbd592b3bf9e23ae2efc3223762d962 ---- /dev/null -+++ b/src/main/java/top/leavesmc/leaves/entity/CraftPhotographerManager.java -@@ -0,0 +1,82 @@ -+package top.leavesmc.leaves.entity; -+ -+import com.google.common.collect.Lists; -+import org.bukkit.Location; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import top.leavesmc.leaves.replay.BukkitRecorderOption; -+import top.leavesmc.leaves.replay.RecorderOption; -+import top.leavesmc.leaves.replay.ServerPhotographer; -+ -+import java.util.Collection; -+import java.util.Collections; -+import java.util.UUID; -+ -+public class CraftPhotographerManager implements PhotographerManager { -+ -+ private final Collection photographerViews = Collections.unmodifiableList(Lists.transform(ServerPhotographer.getPhotographers(), ServerPhotographer::getBukkitPlayer)); -+ -+ @Override -+ public @Nullable Photographer getPhotographer(@NotNull UUID uuid) { -+ ServerPhotographer photographer = ServerPhotographer.getPhotographer(uuid); -+ if (photographer != null) { -+ return photographer.getBukkitPlayer(); -+ } -+ return null; -+ } -+ -+ @Override -+ public @Nullable Photographer getPhotographer(@NotNull String id) { -+ ServerPhotographer photographer = ServerPhotographer.getPhotographer(id); -+ if (photographer != null) { -+ return photographer.getBukkitPlayer(); -+ } -+ return null; -+ } -+ -+ @Override -+ public @Nullable Photographer createPhotographer(@NotNull String id, @NotNull Location location) { -+ ServerPhotographer photographer = new ServerPhotographer.PhotographerCreateState(location, id, RecorderOption.createDefaultOption()).createSync(); -+ if (photographer != null) { -+ return photographer.getBukkitPlayer(); -+ } -+ return null; -+ } -+ -+ @Override -+ public @Nullable Photographer createPhotographer(@NotNull String id, @NotNull Location location, @NotNull BukkitRecorderOption recorderOption) { -+ ServerPhotographer photographer = new ServerPhotographer.PhotographerCreateState(location, id, RecorderOption.createFromBukkit(recorderOption)).createSync(); -+ if (photographer != null) { -+ return photographer.getBukkitPlayer(); -+ } -+ return null; -+ } -+ -+ @Override -+ public void removePhotographer(@NotNull String id) { -+ ServerPhotographer photographer = ServerPhotographer.getPhotographer(id); -+ if (photographer != null) { -+ photographer.remove(true); -+ } -+ } -+ -+ @Override -+ public void removePhotographer(@NotNull UUID uuid) { -+ ServerPhotographer photographer = ServerPhotographer.getPhotographer(uuid); -+ if (photographer != null) { -+ photographer.remove(true); -+ } -+ } -+ -+ @Override -+ public void removeAllPhotographers() { -+ for (ServerPhotographer photographer : ServerPhotographer.getPhotographers()) { -+ photographer.remove(true); -+ } -+ } -+ -+ @Override -+ public Collection getPhotographers() { -+ return photographerViews; -+ } -+} -diff --git a/src/main/java/top/leavesmc/leaves/replay/DigestOutputStream.java b/src/main/java/top/leavesmc/leaves/replay/DigestOutputStream.java -new file mode 100644 -index 0000000000000000000000000000000000000000..92a6478b337c4371e61b7e7878b242619398ecdd ---- /dev/null -+++ b/src/main/java/top/leavesmc/leaves/replay/DigestOutputStream.java -@@ -0,0 +1,46 @@ -+package top.leavesmc.leaves.replay; -+ -+import org.jetbrains.annotations.NotNull; -+ -+import java.io.IOException; -+import java.io.OutputStream; -+import java.util.zip.Checksum; -+ -+public class DigestOutputStream extends OutputStream { -+ -+ private final Checksum sum; -+ private final OutputStream out; -+ -+ public DigestOutputStream(OutputStream out, Checksum sum) { -+ this.out = out; -+ this.sum = sum; -+ } -+ -+ @Override -+ public void close() throws IOException { -+ out.close(); -+ } -+ -+ @Override -+ public void flush() throws IOException { -+ out.flush(); -+ } -+ -+ @Override -+ public void write(int b) throws IOException { -+ sum.update(b); -+ out.write(b); -+ } -+ -+ @Override -+ public void write(byte @NotNull [] b) throws IOException { -+ sum.update(b); -+ out.write(b); -+ } -+ -+ @Override -+ public void write(byte @NotNull [] b, int off, int len) throws IOException { -+ sum.update(b, off, len); -+ out.write(b, off, len); -+ } -+} -diff --git a/src/main/java/top/leavesmc/leaves/replay/RecordMetaData.java b/src/main/java/top/leavesmc/leaves/replay/RecordMetaData.java -new file mode 100644 -index 0000000000000000000000000000000000000000..46a86cfce4aa859b8de7c126c22f64a999a4fe7a ---- /dev/null -+++ b/src/main/java/top/leavesmc/leaves/replay/RecordMetaData.java -@@ -0,0 +1,23 @@ -+package top.leavesmc.leaves.replay; -+ -+import java.util.HashSet; -+import java.util.Set; -+import java.util.UUID; -+ -+public class RecordMetaData { -+ -+ public static final int CURRENT_FILE_FORMAT_VERSION = 14; -+ -+ public boolean singleplayer = false; -+ public String serverName = "Leaves"; -+ public int duration = 0; -+ public long date; -+ public String mcversion; -+ public String fileFormat = "MCPR"; -+ public int fileFormatVersion; -+ public int protocol; -+ public String generator; -+ public int selfId = -1; -+ -+ public Set players = new HashSet<>(); -+} -diff --git a/src/main/java/top/leavesmc/leaves/replay/Recorder.java b/src/main/java/top/leavesmc/leaves/replay/Recorder.java -new file mode 100644 -index 0000000000000000000000000000000000000000..94b67eeb0018f2a31afa372fc44b1af06af6eb72 ---- /dev/null -+++ b/src/main/java/top/leavesmc/leaves/replay/Recorder.java -@@ -0,0 +1,263 @@ -+package top.leavesmc.leaves.replay; -+ -+import io.netty.channel.local.LocalChannel; -+import net.minecraft.SharedConstants; -+import net.minecraft.core.LayeredRegistryAccess; -+import net.minecraft.core.RegistryAccess; -+import net.minecraft.core.RegistrySynchronization; -+import net.minecraft.network.Connection; -+import net.minecraft.network.ConnectionProtocol; -+import net.minecraft.network.PacketSendListener; -+import net.minecraft.network.protocol.Packet; -+import net.minecraft.network.protocol.PacketFlow; -+import net.minecraft.network.protocol.common.ClientboundCustomPayloadPacket; -+import net.minecraft.network.protocol.common.ClientboundDisconnectPacket; -+import net.minecraft.network.protocol.common.ClientboundUpdateTagsPacket; -+import net.minecraft.network.protocol.common.custom.BrandPayload; -+import net.minecraft.network.protocol.configuration.ClientboundFinishConfigurationPacket; -+import net.minecraft.network.protocol.configuration.ClientboundRegistryDataPacket; -+import net.minecraft.network.protocol.configuration.ClientboundUpdateEnabledFeaturesPacket; -+import net.minecraft.network.protocol.game.ClientboundAddEntityPacket; -+import net.minecraft.network.protocol.game.ClientboundBundlePacket; -+import net.minecraft.network.protocol.game.ClientboundGameEventPacket; -+import net.minecraft.network.protocol.game.ClientboundPlayerChatPacket; -+import net.minecraft.network.protocol.game.ClientboundSetTimePacket; -+import net.minecraft.network.protocol.game.ClientboundSystemChatPacket; -+import net.minecraft.network.protocol.login.ClientboundGameProfilePacket; -+import net.minecraft.server.MinecraftServer; -+import net.minecraft.server.RegistryLayer; -+import net.minecraft.tags.TagNetworkSerialization; -+import net.minecraft.world.entity.EntityType; -+import net.minecraft.world.flag.FeatureFlags; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import org.slf4j.Logger; -+import java.io.File; -+import java.io.IOException; -+import java.util.concurrent.CompletableFuture; -+import java.util.concurrent.CompletionException; -+import java.util.concurrent.ExecutorService; -+import java.util.concurrent.Executors; -+import java.util.concurrent.TimeUnit; -+ -+public class Recorder extends Connection { -+ -+ private static final Logger LOGGER = MinecraftServer.LOGGER; -+ -+ private final ReplayFile replayFile; -+ private final ServerPhotographer photographer; -+ private final RecorderOption recorderOption; -+ private final RecordMetaData metaData; -+ -+ private final ExecutorService saveService = Executors.newSingleThreadExecutor(); -+ -+ private boolean stopped = false; -+ private boolean paused = false; -+ private boolean resumeOnNextPacket = true; -+ -+ private long startTime; -+ private long lastPacket; -+ 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 { -+ super(PacketFlow.CLIENTBOUND); -+ -+ this.photographer = photographer; -+ this.recorderOption = recorderOption; -+ this.metaData = new RecordMetaData(); -+ this.replayFile = new ReplayFile(replayFile); -+ this.channel = new LocalChannel(); -+ } -+ -+ public void start() { -+ startTime = System.currentTimeMillis(); -+ -+ metaData.singleplayer = false; -+ metaData.serverName = recorderOption.serverName; -+ metaData.generator = "leaves"; -+ metaData.date = startTime; -+ metaData.mcversion = SharedConstants.getCurrentVersion().getName(); -+ -+ // TODO start event -+ savePacket(new ClientboundGameProfilePacket(photographer.getGameProfile()), ConnectionProtocol.LOGIN); -+ startConfiguration(); -+ -+ if (recorderOption.forceWeather != null) { -+ setWeather(recorderOption.forceWeather); -+ } -+ } -+ -+ public void startConfiguration() { -+ state = ConnectionProtocol.CONFIGURATION; -+ MinecraftServer server = MinecraftServer.getServer(); -+ savePacket(new ClientboundCustomPayloadPacket(new BrandPayload(server.getServerModName())), ConnectionProtocol.CONFIGURATION); -+ LayeredRegistryAccess layeredregistryaccess = server.registries(); -+ -+ savePacket(new ClientboundUpdateEnabledFeaturesPacket(FeatureFlags.REGISTRY.toNames(server.getWorldData().enabledFeatures())), ConnectionProtocol.CONFIGURATION); -+ savePacket(new ClientboundRegistryDataPacket((new RegistryAccess.ImmutableRegistryAccess(RegistrySynchronization.networkedRegistries(layeredregistryaccess))).freeze()), ConnectionProtocol.CONFIGURATION); -+ savePacket(new ClientboundUpdateTagsPacket(TagNetworkSerialization.serializeTagsToNetwork(layeredregistryaccess)), ConnectionProtocol.CONFIGURATION); -+ savePacket(new ClientboundFinishConfigurationPacket(), ConnectionProtocol.CONFIGURATION); -+ state = ConnectionProtocol.PLAY; -+ } -+ -+ @Override -+ public void flushChannel() { -+ } -+ -+ public void stop() { -+ stopped = true; -+ } -+ -+ public void pauseRecording() { -+ resumeOnNextPacket = false; -+ paused = true; -+ } -+ -+ public void resumeRecording() { -+ resumeOnNextPacket = true; -+ } -+ -+ public void setWeather(RecorderOption.RecordWeather weather) { -+ weather.getPackets().forEach(this::savePacket); -+ } -+ -+ public long getRecordedTime() { -+ final long base = System.currentTimeMillis() - startTime; -+ return base - timeShift; -+ } -+ -+ private synchronized long getCurrentTimeAndUpdate() { -+ long now = getRecordedTime(); -+ if (paused) { -+ if (resumeOnNextPacket) { -+ paused = false; -+ } -+ timeShift += now - lastPacket; -+ return lastPacket; -+ } -+ return lastPacket = now; -+ } -+ -+ @Override -+ public boolean isConnected() { -+ return true; -+ } -+ -+ @Override -+ public void send(@NotNull Packet packet, @Nullable PacketSendListener callbacks, boolean flush) { -+ if (!stopped) { -+ if (packet instanceof ClientboundBundlePacket packet1) { -+ packet1.subPackets().forEach(subPacket -> { -+ send(subPacket, null); -+ }); -+ } -+ -+ if (packet instanceof ClientboundAddEntityPacket packet1) { -+ if (packet1.getType() == EntityType.PLAYER) { -+ metaData.players.add(packet1.getUUID()); -+ saveMetadata(); -+ } -+ } -+ -+ if (packet instanceof ClientboundDisconnectPacket) { -+ return; -+ } -+ -+ if (recorderOption.forceDayTime != -1 && packet instanceof ClientboundSetTimePacket packet1) { -+ packet = new ClientboundSetTimePacket(packet1.getDayTime(), 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() { -+ saveService.submit(() -> { -+ try { -+ replayFile.saveMetaData(metaData); -+ } catch (IOException e) { -+ e.printStackTrace(); -+ } -+ }); -+ } -+ -+ private void savePacket(Packet packet) { -+ this.savePacket(packet, state); -+ } -+ -+ private void savePacket(Packet packet, final ConnectionProtocol protocol) { -+ try { -+ final long timestamp = getCurrentTimeAndUpdate(); -+ saveService.submit(() -> { -+ try { -+ replayFile.savePacket(timestamp, packet, protocol); -+ } catch (Exception e) { -+ LOGGER.error("Error saving packet"); -+ e.printStackTrace(); -+ } -+ }); -+ } catch (Exception e) { -+ LOGGER.error("Error saving packet"); -+ e.printStackTrace(); -+ } -+ } -+ -+ public boolean isSaved() { -+ return isSaved; -+ } -+ -+ public CompletableFuture saveRecording(File dest, boolean save) { -+ isSaved = true; -+ if (!isSaving) { -+ isSaving = true; -+ metaData.duration = (int) lastPacket; -+ return CompletableFuture.runAsync(() -> { -+ saveMetadata(); -+ saveService.shutdown(); -+ boolean interrupted = false; -+ try { -+ saveService.awaitTermination(10, TimeUnit.SECONDS); -+ } catch (InterruptedException e) { -+ interrupted = true; -+ } -+ 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 { -+ LOGGER.warn("saveRecording() called twice"); -+ return CompletableFuture.supplyAsync(() -> { -+ throw new IllegalStateException("saveRecording() called twice"); -+ }); -+ } -+ } -+} -diff --git a/src/main/java/top/leavesmc/leaves/replay/RecorderOption.java b/src/main/java/top/leavesmc/leaves/replay/RecorderOption.java -new file mode 100644 -index 0000000000000000000000000000000000000000..06e7166336d621e1a8edb4a2ad88e2cb8a52abb1 ---- /dev/null -+++ b/src/main/java/top/leavesmc/leaves/replay/RecorderOption.java -@@ -0,0 +1,57 @@ -+package top.leavesmc.leaves.replay; -+ -+import net.minecraft.network.protocol.Packet; -+import net.minecraft.network.protocol.game.ClientboundGameEventPacket; -+import org.jetbrains.annotations.Contract; -+import org.jetbrains.annotations.NotNull; -+ -+import java.util.List; -+ -+public class RecorderOption { -+ -+ public int recordDistance = -1; -+ public String serverName = "Leaves"; -+ public RecordWeather forceWeather = null; -+ public int forceDayTime = -1; -+ public boolean ignoreChat = false; -+ public boolean ignoreItem = false; -+ -+ @NotNull -+ @Contract(" -> new") -+ public static RecorderOption createDefaultOption() { -+ return new RecorderOption(); -+ } -+ -+ @NotNull -+ public static RecorderOption createFromBukkit(@NotNull BukkitRecorderOption bukkitRecorderOption) { -+ RecorderOption recorderOption = new RecorderOption(); -+ // recorderOption.recordDistance = bukkitRecorderOption.recordDistance; -+ // recorderOption.ignoreItem = bukkitRecorderOption.ignoreItem; -+ recorderOption.serverName = bukkitRecorderOption.serverName; -+ recorderOption.ignoreChat = bukkitRecorderOption.ignoreChat; -+ recorderOption.forceDayTime = bukkitRecorderOption.forceDayTime; -+ recorderOption.forceWeather = switch (bukkitRecorderOption.forceWeather) { -+ case RAIN -> RecordWeather.RAIN; -+ case CLEAR -> RecordWeather.CLEAR; -+ case THUNDER -> RecordWeather.THUNDER; -+ case NULL -> null; -+ }; -+ return recorderOption; -+ } -+ -+ public enum RecordWeather { -+ CLEAR(new ClientboundGameEventPacket(ClientboundGameEventPacket.STOP_RAINING, 0), new ClientboundGameEventPacket(ClientboundGameEventPacket.RAIN_LEVEL_CHANGE, 0), new ClientboundGameEventPacket(ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE, 0)), -+ RAIN(new ClientboundGameEventPacket(ClientboundGameEventPacket.START_RAINING, 0), new ClientboundGameEventPacket(ClientboundGameEventPacket.RAIN_LEVEL_CHANGE, 1), new ClientboundGameEventPacket(ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE, 0)), -+ THUNDER(new ClientboundGameEventPacket(ClientboundGameEventPacket.START_RAINING, 0), new ClientboundGameEventPacket(ClientboundGameEventPacket.RAIN_LEVEL_CHANGE, 1), new ClientboundGameEventPacket(ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE, 1)); -+ -+ private final List> packets; -+ -+ private RecordWeather(Packet... packets) { -+ this.packets = List.of(packets); -+ } -+ -+ public List> getPackets() { -+ return packets; -+ } -+ } -+} -diff --git a/src/main/java/top/leavesmc/leaves/replay/ReplayFile.java b/src/main/java/top/leavesmc/leaves/replay/ReplayFile.java -new file mode 100644 -index 0000000000000000000000000000000000000000..8cb0f7e2c9f6b7455541c5e06acca0e1ceb60a11 ---- /dev/null -+++ b/src/main/java/top/leavesmc/leaves/replay/ReplayFile.java -@@ -0,0 +1,178 @@ -+package top.leavesmc.leaves.replay; -+ -+import com.google.gson.Gson; -+import com.google.gson.GsonBuilder; -+import io.netty.buffer.ByteBuf; -+import io.netty.buffer.Unpooled; -+import net.minecraft.SharedConstants; -+import net.minecraft.network.ConnectionProtocol; -+import net.minecraft.network.FriendlyByteBuf; -+import net.minecraft.network.protocol.Packet; -+import net.minecraft.network.protocol.PacketFlow; -+import org.jetbrains.annotations.NotNull; -+import top.leavesmc.leaves.util.UUIDSerializer; -+ -+import java.io.BufferedOutputStream; -+import java.io.DataOutputStream; -+import java.io.File; -+import java.io.FileInputStream; -+import java.io.FileOutputStream; -+import java.io.IOException; -+import java.io.InputStream; -+import java.io.OutputStream; -+import java.io.OutputStreamWriter; -+import java.io.Writer; -+import java.nio.charset.StandardCharsets; -+import java.nio.file.Files; -+import java.util.List; -+import java.util.UUID; -+import java.util.zip.CRC32; -+import java.util.zip.ZipEntry; -+import java.util.zip.ZipOutputStream; -+ -+public class ReplayFile { -+ -+ private static final String RECORDING_FILE = "recording.tmcpr"; -+ private static final String RECORDING_FILE_CRC32 = "recording.tmcpr.crc32"; -+ private static final String MARKER_FILE = "markers.json"; -+ private static final String META_FILE = "metaData.json"; -+ -+ private static final Gson MARKER_GSON = new GsonBuilder().registerTypeAdapter(ReplayMarker.class, new ReplayMarker.Serializer()).create(); -+ private static final Gson META_GSON = new GsonBuilder().registerTypeAdapter(UUID.class, new UUIDSerializer()).create(); -+ -+ private final File tmpDir; -+ private final DataOutputStream packetStream; -+ private final CRC32 crc32 = new CRC32(); -+ -+ private final File markerFile; -+ private final File metaFile; -+ -+ public ReplayFile(@NotNull File name) throws IOException { -+ this.tmpDir = new File(name.getParentFile(), name.getName() + ".tmp"); -+ if (tmpDir.exists()) { -+ if (!ReplayFile.deleteDir(tmpDir)) { -+ throw new IOException("Recording file " + name + " already exists!"); -+ } -+ } -+ -+ if (!tmpDir.mkdirs()) { -+ throw new IOException("Failed to create temp directory for recording " + tmpDir); -+ } -+ -+ File packetFile = new File(tmpDir, RECORDING_FILE); -+ metaFile = new File(tmpDir, META_FILE); -+ markerFile = new File(tmpDir, MARKER_FILE); -+ -+ packetStream = new DataOutputStream(new DigestOutputStream(new BufferedOutputStream(new FileOutputStream(packetFile)), crc32)); -+ } -+ -+ private byte @NotNull [] getPacketBytes(Packet packet, ConnectionProtocol state) { -+ int packetID = state.codec(PacketFlow.CLIENTBOUND).packetId(packet); -+ ByteBuf buf = Unpooled.buffer(); -+ FriendlyByteBuf packetBuf = new FriendlyByteBuf(buf); -+ packetBuf.writeVarInt(packetID); -+ packet.write(packetBuf); -+ -+ buf.readerIndex(0); -+ byte[] ret = new byte[buf.readableBytes()]; -+ buf.readBytes(ret); -+ buf.release(); -+ return ret; -+ } -+ -+ public void saveMarkers(List markers) throws IOException { -+ try (Writer writer = new OutputStreamWriter(new FileOutputStream(markerFile), StandardCharsets.UTF_8)) { -+ writer.write(MARKER_GSON.toJson(markers)); -+ } -+ } -+ -+ public void saveMetaData(@NotNull RecordMetaData data) throws IOException { -+ data.fileFormat = "MCPR"; -+ data.fileFormatVersion = RecordMetaData.CURRENT_FILE_FORMAT_VERSION; -+ data.protocol = SharedConstants.getCurrentVersion().getProtocolVersion(); -+ -+ try (Writer writer = new OutputStreamWriter(new FileOutputStream(metaFile), StandardCharsets.UTF_8)) { -+ writer.write(META_GSON.toJson(data)); -+ } -+ } -+ -+ public void savePacket(long timestamp, Packet packet, ConnectionProtocol protocol) throws Exception { -+ byte[] data = getPacketBytes(packet, protocol); -+ packetStream.writeInt((int) timestamp); -+ packetStream.writeInt(data.length); -+ packetStream.write(data); -+ } -+ -+ public synchronized void closeAndSave(File file) throws IOException { -+ packetStream.close(); -+ -+ String[] files = tmpDir.list(); -+ if (files == null) { -+ return; -+ } -+ -+ try (ZipOutputStream os = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(file)))) { -+ for (String fileName : files) { -+ os.putNextEntry(new ZipEntry(fileName)); -+ File f = new File(tmpDir, fileName); -+ copy(new FileInputStream(f), os); -+ } -+ -+ os.putNextEntry(new ZipEntry(RECORDING_FILE_CRC32)); -+ Writer writer = new OutputStreamWriter(os); -+ writer.write(Long.toString(crc32.getValue())); -+ writer.flush(); -+ } -+ -+ for (String fileName : files) { -+ File f = new File(tmpDir, fileName); -+ Files.delete(f.toPath()); -+ } -+ Files.delete(tmpDir.toPath()); -+ } -+ -+ public synchronized void closeNotSave() throws IOException { -+ packetStream.close(); -+ -+ String[] files = tmpDir.list(); -+ if (files == null) { -+ return; -+ } -+ -+ for (String fileName : files) { -+ File f = new File(tmpDir, fileName); -+ Files.delete(f.toPath()); -+ } -+ Files.delete(tmpDir.toPath()); -+ } -+ -+ private void copy(@NotNull InputStream in, OutputStream out) throws IOException { -+ byte[] buffer = new byte[8192]; -+ int len; -+ while ((len = in.read(buffer)) > -1) { -+ out.write(buffer, 0, len); -+ } -+ in.close(); -+ } -+ -+ private static boolean deleteDir(File dir) { -+ if (dir == null || !dir.exists()) { -+ return false; -+ } -+ -+ File[] files = dir.listFiles(); -+ if (files != null) { -+ for (File file : files) { -+ if (file.isDirectory()) { -+ deleteDir(file); -+ } else { -+ if (!file.delete()) { -+ return false; -+ } -+ } -+ } -+ } -+ -+ return dir.delete(); -+ } -+} -diff --git a/src/main/java/top/leavesmc/leaves/replay/ReplayMarker.java b/src/main/java/top/leavesmc/leaves/replay/ReplayMarker.java -new file mode 100644 -index 0000000000000000000000000000000000000000..852f2098d93d4437fe79af06e454d8494b6decf1 ---- /dev/null -+++ b/src/main/java/top/leavesmc/leaves/replay/ReplayMarker.java -@@ -0,0 +1,43 @@ -+package top.leavesmc.leaves.replay; -+ -+import com.google.gson.JsonElement; -+import com.google.gson.JsonObject; -+import com.google.gson.JsonPrimitive; -+import com.google.gson.JsonSerializationContext; -+import com.google.gson.JsonSerializer; -+ -+import java.lang.reflect.Type; -+ -+public class ReplayMarker { -+ -+ public int time; -+ public String name; -+ public double x = 0; -+ public double y = 0; -+ public double z = 0; -+ public float phi = 0; -+ public float theta = 0; -+ public float varphi = 0; -+ -+ public static class Serializer implements JsonSerializer { -+ @Override -+ public JsonElement serialize(ReplayMarker src, Type typeOfSrc, JsonSerializationContext context) { -+ JsonObject ret = new JsonObject(); -+ JsonObject value = new JsonObject(); -+ JsonObject position = new JsonObject(); -+ ret.add("realTimestamp", new JsonPrimitive(src.time)); -+ ret.add("value", value); -+ -+ value.add("name", new JsonPrimitive(src.name)); -+ value.add("position", position); -+ -+ position.add("x", new JsonPrimitive(src.x)); -+ position.add("y", new JsonPrimitive(src.y)); -+ position.add("z", new JsonPrimitive(src.z)); -+ position.add("yaw", new JsonPrimitive(src.phi)); -+ position.add("pitch", new JsonPrimitive(src.theta)); -+ position.add("roll", new JsonPrimitive(src.varphi)); -+ return ret; -+ } -+ } -+} -diff --git a/src/main/java/top/leavesmc/leaves/replay/ServerPhotographer.java b/src/main/java/top/leavesmc/leaves/replay/ServerPhotographer.java -new file mode 100644 -index 0000000000000000000000000000000000000000..438651c02b371f9f85cd97fe0fefb14d1858aaa0 ---- /dev/null -+++ b/src/main/java/top/leavesmc/leaves/replay/ServerPhotographer.java -@@ -0,0 +1,246 @@ -+package top.leavesmc.leaves.replay; -+ -+import com.mojang.authlib.GameProfile; -+import io.papermc.paper.threadedregions.TickRegionScheduler; -+import me.earthme.luminol.utils.NullStatsCounter; -+import net.minecraft.network.Connection; -+import net.minecraft.server.MinecraftServer; -+import net.minecraft.server.level.ClientInformation; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.server.level.ServerPlayer; -+import net.minecraft.server.network.ServerGamePacketListenerImpl; -+import net.minecraft.stats.ServerStatsCounter; -+import net.minecraft.world.damagesource.DamageSource; -+import net.minecraft.world.level.GameType; -+import net.minecraft.world.phys.Vec3; -+import org.bukkit.Bukkit; -+import org.bukkit.Location; -+import org.bukkit.craftbukkit.CraftWorld; -+import org.jetbrains.annotations.NotNull; -+import top.leavesmc.leaves.entity.CraftPhotographer; -+import top.leavesmc.leaves.entity.Photographer; -+ -+import java.io.File; -+import java.io.IOException; -+import java.util.List; -+import java.util.UUID; -+import java.util.concurrent.CompletableFuture; -+import java.util.concurrent.CopyOnWriteArrayList; -+ -+public class ServerPhotographer extends ServerPlayer { -+ -+ private static final List photographers = new CopyOnWriteArrayList<>(); -+ -+ public PhotographerCreateState createState; -+ private ServerPlayer followPlayer; -+ public Recorder recorder; -+ private File saveFile; -+ private Vec3 lastPos; -+ -+ private final ServerStatsCounter stats; -+ -+ private ServerPhotographer(MinecraftServer server, ServerLevel world, GameProfile profile) { -+ super(server, world, profile, ClientInformation.createDefault()); -+ this.gameMode = new ServerPhotographerGameMode(this); -+ this.followPlayer = null; -+ this.stats = new NullStatsCounter(server); -+ this.lastPos = this.position(); -+ } -+ -+ public static boolean isCreateLegal(@NotNull String name) { -+ if (!name.matches("^[a-zA-Z0-9_]{4,16}$")) { -+ return false; -+ } -+ -+ if (Bukkit.getPlayer(name) != null) { -+ return false; -+ } -+ -+ return true; -+ } -+ -+ public static ServerPhotographer createPhotographer(@NotNull PhotographerCreateState state) throws IOException { -+ if (!isCreateLegal(state.id)) { -+ return null; -+ } -+ -+ MinecraftServer server = MinecraftServer.getServer(); -+ -+ ServerLevel world = ((CraftWorld) state.loc.getWorld()).getHandle(); -+ GameProfile profile = new GameProfile(UUID.randomUUID(), state.id); -+ -+ ServerPhotographer photographer = new ServerPhotographer(server, world, profile); -+ photographer.recorder = new Recorder(photographer, state.option, new File("replay", state.id)); -+ photographer.saveFile = new File("replay", state.id + ".mcpr"); -+ photographer.createState = state; -+ -+ photographer.recorder.start(); -+ MinecraftServer.getServer().getPlayerList().placeNewPhotographer(photographer.recorder, photographer, world, state.loc); -+ photographer.serverLevel().chunkSource.move(photographer); -+ photographer.setInvisible(true); -+ photographers.add(photographer); -+ -+ MinecraftServer.LOGGER.info("Photographer " + state.id + " created"); -+ -+ // TODO record distance -+ -+ return photographer; -+ } -+ -+ @Override -+ public void tick() { -+ super.tick(); -+ super.doTick(); -+ -+ if (TickRegionScheduler.getCurrentRegion().getData().getCurrentTick() % 10 == 0) { -+ connection.resetPosition(); -+ this.serverLevel().chunkSource.move(this); -+ } -+ -+ CompletableFuture teleportAction = null; -+ boolean shouldUpdateCameraAfterTeleportation; -+ -+ if (this.followPlayer != null) { -+ if (this.getCamera() == this || this.getCamera().level() != this.level()) { -+ teleportAction = this.getBukkitPlayer().teleportAsync(this.getCamera().getBukkitEntity().getLocation()); -+ shouldUpdateCameraAfterTeleportation = true; -+ } else { -+ shouldUpdateCameraAfterTeleportation = false; -+ } -+ -+ if (lastPos.distanceToSqr(this.position()) > 1024D) { -+ teleportAction = this.getBukkitPlayer().teleportAsync(this.getCamera().getBukkitEntity().getLocation()); -+ } -+ } else { -+ shouldUpdateCameraAfterTeleportation = false; -+ } -+ -+ if (teleportAction != null){ -+ teleportAction.whenComplete((result,ex) -> { -+ if (shouldUpdateCameraAfterTeleportation){ -+ this.setCamera(followPlayer); -+ } -+ -+ lastPos = this.position(); -+ }); -+ return; -+ } -+ lastPos = this.position(); -+ } -+ -+ @Override -+ public void die(@NotNull DamageSource damageSource) { -+ super.die(damageSource); -+ remove(true); -+ } -+ -+ @Override -+ public boolean isInvulnerableTo(@NotNull DamageSource damageSource) { -+ return true; -+ } -+ -+ @Override -+ public boolean hurt(@NotNull DamageSource source, float amount) { -+ return false; -+ } -+ -+ @Override -+ public void setHealth(float health) { -+ } -+ -+ @NotNull -+ @Override -+ public ServerStatsCounter getStats() { -+ return stats; -+ } -+ -+ public void remove(boolean async) { -+ this.remove(async, true); -+ } -+ -+ public void remove(boolean async, boolean save) { -+ super.remove(RemovalReason.KILLED); -+ photographers.remove(this); -+ this.recorder.stop(); -+ this.server.getPlayerList().removePhotographer(this).thenAccept(nullResult -> { -+ MinecraftServer.LOGGER.info("Photographer " + createState.id + " removed"); -+ if (save && !recorder.isSaved()) { -+ CompletableFuture future = recorder.saveRecording(saveFile, save); -+ if (!async) { -+ future.join(); -+ } -+ } -+ }); -+ } -+ -+ public void setFollowPlayer(ServerPlayer followPlayer) { -+ this.setCamera(followPlayer); -+ this.followPlayer = followPlayer; -+ } -+ -+ public void setSaveFile(File saveFile) { -+ this.saveFile = saveFile; -+ } -+ -+ public void pauseRecording() { -+ this.recorder.pauseRecording(); -+ } -+ -+ public void resumeRecording() { -+ this.recorder.resumeRecording(); -+ } -+ -+ public static ServerPhotographer getPhotographer(String id) { -+ for (ServerPhotographer photographer : photographers) { -+ if (photographer.createState.id.equals(id)) { -+ return photographer; -+ } -+ } -+ return null; -+ } -+ -+ public static ServerPhotographer getPhotographer(UUID uuid) { -+ for (ServerPhotographer photographer : photographers) { -+ if (photographer.getUUID().equals(uuid)) { -+ return photographer; -+ } -+ } -+ return null; -+ } -+ -+ public static List getPhotographers() { -+ return photographers; -+ } -+ -+ public Photographer getBukkitPlayer() { -+ return getBukkitEntity(); -+ } -+ -+ @Override -+ @NotNull -+ public CraftPhotographer getBukkitEntity() { -+ return (CraftPhotographer) super.getBukkitEntity(); -+ } -+ -+ public static class PhotographerCreateState { -+ -+ public RecorderOption option; -+ public Location loc; -+ public final String id; -+ -+ public PhotographerCreateState(Location loc, String id, RecorderOption option) { -+ this.loc = loc; -+ this.id = id; -+ this.option = option; -+ } -+ -+ public ServerPhotographer createSync() { -+ try { -+ return createPhotographer(this); -+ } catch (IOException e) { -+ e.printStackTrace(); -+ } -+ return null; -+ } -+ } -+} -diff --git a/src/main/java/top/leavesmc/leaves/replay/ServerPhotographerGameMode.java b/src/main/java/top/leavesmc/leaves/replay/ServerPhotographerGameMode.java -new file mode 100644 -index 0000000000000000000000000000000000000000..f8c4d248c7f9e0b42cd04e252a1281ceb3e3c4ce ---- /dev/null -+++ b/src/main/java/top/leavesmc/leaves/replay/ServerPhotographerGameMode.java -@@ -0,0 +1,34 @@ -+package top.leavesmc.leaves.replay; -+ -+ import net.kyori.adventure.text.Component; -+import net.minecraft.server.level.ServerPlayerGameMode; -+import net.minecraft.world.level.GameType; -+import org.bukkit.event.player.PlayerGameModeChangeEvent; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+ -+public class ServerPhotographerGameMode extends ServerPlayerGameMode { -+ public ServerPhotographerGameMode(ServerPhotographer photographer) { -+ super(photographer); -+ super.setGameModeForPlayer(GameType.SPECTATOR, null); -+ } -+ -+ @Override -+ public boolean changeGameModeForPlayer(@NotNull GameType gameMode) { -+ return false; -+ } -+ -+ @Nullable -+ @Override -+ public PlayerGameModeChangeEvent changeGameModeForPlayer(@NotNull GameType gameMode, PlayerGameModeChangeEvent.@NotNull Cause cause, @Nullable Component cancelMessage) { -+ return null; -+ } -+ -+ @Override -+ protected void setGameModeForPlayer(@NotNull GameType gameMode, @Nullable GameType previousGameMode) { -+ } -+ -+ @Override -+ public void tick() { -+ } -+} -\ No newline at end of file -diff --git a/src/main/java/top/leavesmc/leaves/util/UUIDSerializer.java b/src/main/java/top/leavesmc/leaves/util/UUIDSerializer.java -new file mode 100644 -index 0000000000000000000000000000000000000000..1329a725a2bd03d3ef6d7131d8bc77f20bf2e566 ---- /dev/null -+++ b/src/main/java/top/leavesmc/leaves/util/UUIDSerializer.java -@@ -0,0 +1,17 @@ -+package top.leavesmc.leaves.util; -+ -+import com.google.gson.JsonElement; -+import com.google.gson.JsonPrimitive; -+import com.google.gson.JsonSerializationContext; -+import com.google.gson.JsonSerializer; -+import org.jetbrains.annotations.NotNull; -+ -+import java.lang.reflect.Type; -+import java.util.UUID; -+ -+public class UUIDSerializer implements JsonSerializer { -+ @Override -+ public JsonElement serialize(@NotNull UUID src, Type typeOfSrc, JsonSerializationContext context) { -+ return new JsonPrimitive(src.toString()); -+ } -+}