From ba3825f69c91d4b3c38365b0f1726985af815747 Mon Sep 17 00:00:00 2001 From: HEBEI77 Date: Wed, 9 Sep 2026 01:51:28 +0000 Subject: [PATCH] fix: sweep map holder state of departed players --- .../server/MinecraftServer.java.patch | 9 +++ .../server/players/PlayerList.java.patch | 5 +- .../maps/MapItemSavedData.java.patch | 30 +++++++++ .../level/storage/SavedDataStorage.java.patch | 19 ++++++ .../papermc/paper/util/MapHolderCleanup.java | 64 +++++++++++++++++++ 5 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 paper-server/src/main/java/io/papermc/paper/util/MapHolderCleanup.java diff --git a/paper-server/patches/sources/net/minecraft/server/MinecraftServer.java.patch b/paper-server/patches/sources/net/minecraft/server/MinecraftServer.java.patch index a91ebfae2028..02d5336c1a87 100644 --- a/paper-server/patches/sources/net/minecraft/server/MinecraftServer.java.patch +++ b/paper-server/patches/sources/net/minecraft/server/MinecraftServer.java.patch @@ -1031,6 +1031,15 @@ profiler.push("tallying"); long tickTime = Util.getNanos() - nano; int tickIndex = this.tickCount % 100; +@@ -1005,5 +_,8 @@ + this.logTickMethodTime(nano); + profiler.pop(); + } ++ // Paper start - sweep map holders of departed players ++ io.papermc.paper.util.MapHolderCleanup.tick(this); ++ // Paper end + + protected void processPacketsAndTick(final boolean sprinting) { @@ -1012,16 +_,16 @@ ProfilerFiller profiler = Profiler.get(); profiler.push("tick"); diff --git a/paper-server/patches/sources/net/minecraft/server/players/PlayerList.java.patch b/paper-server/patches/sources/net/minecraft/server/players/PlayerList.java.patch index aee225f04858..5fedb7801fa8 100644 --- a/paper-server/patches/sources/net/minecraft/server/players/PlayerList.java.patch +++ b/paper-server/patches/sources/net/minecraft/server/players/PlayerList.java.patch @@ -269,7 +269,7 @@ } @Override -@@ -292,65 +_,146 @@ +@@ -292,65 +_,149 @@ } protected void save(final ServerPlayer player) { @@ -398,6 +398,9 @@ + // This removes the scoreboard (and player reference) for the specific player in the manager + this.cserver.getScoreboardManager().removePlayer(player.getBukkitEntity()); + // CraftBukkit end ++ // Paper start - queue map holder cleanup for departed player ++ io.papermc.paper.util.MapHolderCleanup.onPlayerDisconnect(uuid); ++ // Paper end + return playerQuitEvent.quitMessage(); // Paper - Adventure + } + diff --git a/paper-server/patches/sources/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java.patch b/paper-server/patches/sources/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java.patch index e674e94cd9d0..eeb689d21cb4 100644 --- a/paper-server/patches/sources/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java.patch +++ b/paper-server/patches/sources/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java.patch @@ -1,5 +1,12 @@ --- a/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java +++ b/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java +@@ -12,4 +_,6 @@ + import java.util.Map; + import java.util.Objects; + import java.util.Optional; ++import java.util.Set; ++import java.util.UUID; + import java.util.function.Predicate; @@ -48,7 +_,7 @@ private static final String FRAME_PREFIX = "frame-"; public static final Codec CODEC = RecordCodecBuilder.create( @@ -108,6 +115,29 @@ for (MapItemSavedData.HoldingPlayer holdingPlayer : this.carriedBy) { holdingPlayer.markColorsDirty(x, y); +@@ -388,8 +_,22 @@ + this.carriedByPlayers.put(player, holdingPlayer); + this.carriedBy.add(holdingPlayer); + } + + return holdingPlayer; + } ++ ++ // Paper start - remove holder state for departed players ++ public void removeHolders(final Set playerIds) { ++ Iterator iterator = this.carriedBy.iterator(); ++ while (iterator.hasNext()) { ++ HoldingPlayer holder = iterator.next(); ++ if (playerIds.contains(holder.player.getUUID())) { ++ this.carriedByPlayers.remove(holder.player); ++ iterator.remove(); ++ this.removeDecoration(holder.player.getPlainTextName()); ++ } ++ } ++ } ++ // Paper end + + public boolean toggleBanner(final LevelAccessor level, final BlockPos pos) { @@ -416,7 +_,7 @@ return true; } diff --git a/paper-server/patches/sources/net/minecraft/world/level/storage/SavedDataStorage.java.patch b/paper-server/patches/sources/net/minecraft/world/level/storage/SavedDataStorage.java.patch index 181dbcff0089..26b0a0b19d03 100644 --- a/paper-server/patches/sources/net/minecraft/world/level/storage/SavedDataStorage.java.patch +++ b/paper-server/patches/sources/net/minecraft/world/level/storage/SavedDataStorage.java.patch @@ -1,5 +1,24 @@ --- a/net/minecraft/world/level/storage/SavedDataStorage.java +++ b/net/minecraft/world/level/storage/SavedDataStorage.java +@@ -104,8 +_,18 @@ + } + + public void set(final SavedDataType type, final T data) { + this.cache.put(type, Optional.of(data)); + data.setDirty(); + } ++ ++ // Paper start - expose loaded data for map holder cleanup ++ public List getLoadedData() { ++ List loaded = new ArrayList<>(); ++ for (Optional data : this.cache.values()) { ++ data.ifPresent(loaded::add); ++ } ++ return loaded; ++ } ++ // Paper end + + public CompoundTag readTagFromDisk(final Path dataFile, final DataFixTypes type, final int newVersion) throws IOException { @@ -155,7 +_,7 @@ int threads = Util.maxAllowedExecutorThreads(); diff --git a/paper-server/src/main/java/io/papermc/paper/util/MapHolderCleanup.java b/paper-server/src/main/java/io/papermc/paper/util/MapHolderCleanup.java new file mode 100644 index 000000000000..df67672d66d7 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/util/MapHolderCleanup.java @@ -0,0 +1,64 @@ +package io.papermc.paper.util; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.saveddata.SavedData; +import net.minecraft.world.level.saveddata.maps.MapItemSavedData; + +/** + * Removes per-player map holder state for departed players. + * + *

Vanilla only drops {@code MapItemSavedData} holder entries while a map + * ticks for an online player, so disconnected players are retained - along + * with their whole {@code ServerPlayer} - until the map data unloads. + * Disconnects are collected here and swept in one batched pass, so quit + * storms cost a single catalogue scan instead of one per player.

+ */ +public final class MapHolderCleanup { + private static final int SWEEP_INTERVAL_TICKS = 100; // 5 seconds + private static final Set pendingDepartures = ConcurrentHashMap.newKeySet(); + private static int lastSweepTick; + + private MapHolderCleanup() { + } + + public static void onPlayerDisconnect(final UUID playerId) { + pendingDepartures.add(playerId); + } + + public static void tick(final MinecraftServer server) { + if (pendingDepartures.isEmpty()) { + return; + } + int tick = server.getTickCount(); + if (tick - lastSweepTick < SWEEP_INTERVAL_TICKS) { + return; + } + lastSweepTick = tick; + + Set departed = Set.copyOf(pendingDepartures); + pendingDepartures.removeAll(departed); + + Set offline = new HashSet<>(); + for (UUID uuid : departed) { + if (server.getPlayerList().getPlayer(uuid) == null) { + offline.add(uuid); + } + } + if (offline.isEmpty()) { + return; + } + + for (ServerLevel level : server.getAllLevels()) { + for (SavedData data : level.getDataStorage().getLoadedData()) { + if (data instanceof MapItemSavedData mapData) { + mapData.removeHolders(offline); + } + } + } + } +}