diff --git a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/border/animation/block/BorderBlockController.java b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/border/animation/block/BorderBlockController.java index 73e2fb95..70fd12da 100644 --- a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/border/animation/block/BorderBlockController.java +++ b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/border/animation/block/BorderBlockController.java @@ -30,6 +30,7 @@ import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerQuitEvent; public class BorderBlockController implements Listener { @@ -46,6 +47,7 @@ public class BorderBlockController implements Listener { private final Set playersToUpdate = ConcurrentHashMap.newKeySet(); private final Map lockedPlayers = new ConcurrentHashMap<>(); + private final Map> originalBlocks = new ConcurrentHashMap<>(); private final ChunkCache chunkCache; public BorderBlockController(BorderService borderService, Supplier settings, Scheduler scheduler, Server server) { @@ -54,42 +56,59 @@ public BorderBlockController(BorderService borderService, Supplier this.updatePlayers(), settings.get().updateDelay, settings.get().updateDelay); + scheduler.timerAsync(this::updatePlayers, settings.get().updateDelay, settings.get().updateDelay); } @EventHandler void onBorderShowAsyncEvent(BorderShowAsyncEvent event) { - if (!settings.get().enabled) { + if (!this.settings.get().enabled) { return; } Player player = event.getPlayer(); - Set borderPoints = this.getPointsWithoutAir(player, event.getPoints()); + UUID playerId = player.getUniqueId(); - event.setPoints(borderPoints); - this.showBlocks(player, borderPoints); - this.playersToUpdate.add(player.getUniqueId()); + synchronized (this.getLock(playerId)) { + Set borderPoints = this.filterPassablePoints(player, event.getPoints()); + + event.setPoints(borderPoints); + this.showBlocks(player, borderPoints); + this.playersToUpdate.add(playerId); + } } @EventHandler void onBorderHideAsyncEvent(BorderHideAsyncEvent event) { - if (!settings.get().enabled) { + if (!this.settings.get().enabled) { return; } - Object lock = lockedPlayers.computeIfAbsent(event.getPlayer().getUniqueId(), k -> new Object()); + UUID playerId = event.getPlayer().getUniqueId(); + + Object lock = this.getLock(playerId); + synchronized (lock) { - this.hideBlocks(event.getPlayer(), event.getPoints()); + this.restoreBlocks(event.getPlayer(), event.getPoints()); Set border = this.borderService.getActiveBorder(event.getPlayer()); if (border.isEmpty()) { - this.playersToUpdate.remove(event.getPlayer().getUniqueId()); + this.playersToUpdate.remove(playerId); + this.originalBlocks.remove(playerId); } } } + @EventHandler + void onPlayerQuit(PlayerQuitEvent event) { + UUID playerId = event.getPlayer().getUniqueId(); + + this.playersToUpdate.remove(playerId); + this.originalBlocks.remove(playerId); + this.lockedPlayers.remove(playerId); + } + private void updatePlayers() { - if (!settings.get().enabled) { + if (!this.settings.get().enabled) { return; } @@ -105,93 +124,127 @@ private void updatePlayers() { } private void updatePlayer(UUID uuid, Player player) { - Object lock = lockedPlayers.computeIfAbsent(uuid, k -> new Object()); + Object lock = this.getLock(uuid); + synchronized (lock) { Set border = this.borderService.getActiveBorder(player); if (border.isEmpty()) { this.playersToUpdate.remove(uuid); + this.originalBlocks.remove(uuid); return; } - this.showBlocks(player, border); + Set passablePoints = this.filterPassablePoints(player, border); + this.showBlocks(player, passablePoints); } } private void showBlocks(Player player, Collection blocks) { - this.splitByChunks(blocks).entrySet().stream() - .map(chunkBlocks -> toMultiBlockChangePacket(chunkBlocks)) - .forEach(change -> PLAYER_MANAGER.sendPacket(player, change)); - } + this.splitByChunkSection(blocks).forEach((chunkPos, chunkPoints) -> { + EncodedBlock[] encodedBlocks = chunkPoints.stream() + .map(this::toBorderBlock) + .toArray(EncodedBlock[]::new); - private WrapperPlayServerMultiBlockChange toMultiBlockChangePacket(Entry> chunkBlocks) { - EncodedBlock[] encodedBlocks = chunkBlocks.getValue().stream() - .map(borderPoint -> this.toEncodedBlock(borderPoint)) - .toArray(value -> new EncodedBlock[value]); - - return new WrapperPlayServerMultiBlockChange(chunkBlocks.getKey(), true, encodedBlocks); + PLAYER_MANAGER.sendPacket(player, new WrapperPlayServerMultiBlockChange(chunkPos, true, encodedBlocks)); + }); } - private void hideBlocks(Player player, Collection blocks) { - this.splitByChunks(blocks).entrySet().stream() - .map(chunkBlocks -> toMultiAirChangePacket(chunkBlocks)) - .forEach(change -> PLAYER_MANAGER.sendPacket(player, change)); - } + private void restoreBlocks(Player player, Collection blocks) { + UUID playerId = player.getUniqueId(); + Map savedBlocks = this.originalBlocks.get(playerId); + + this.splitByChunkSection(blocks).forEach((chunkPos, chunkPoints) -> { + EncodedBlock[] encodedBlocks = chunkPoints.stream() + .map(point -> this.toOriginalBlock(point, savedBlocks)) + .toArray(EncodedBlock[]::new); - private WrapperPlayServerMultiBlockChange toMultiAirChangePacket(Entry> chunkBlocks) { - EncodedBlock[] encodedBlocks = chunkBlocks.getValue().stream() - .map(point -> new EncodedBlock(AIR_ID, point.x(), point.y(), point.z())) - .toArray(value -> new EncodedBlock[value]); + PLAYER_MANAGER.sendPacket(player, new WrapperPlayServerMultiBlockChange(chunkPos, true, encodedBlocks)); + }); - return new WrapperPlayServerMultiBlockChange(chunkBlocks.getKey(), true, encodedBlocks); + if (savedBlocks != null) { + blocks.forEach(savedBlocks::remove); + } } - private Set getPointsWithoutAir(Player player, Collection blocks) { - Map> chunksToProcess = blocks.stream() - .map(borderPoint -> borderPoint.toInclusive()) - .collect(Collectors.groupingBy( - inclusive -> new ChunkLocation(inclusive.x() >> MINECRAFT_CHUNK_SHIFT, inclusive.z() >> MINECRAFT_CHUNK_SHIFT), - Collectors.toSet() - )); + private Set filterPassablePoints(Player player, Collection points) { + UUID playerId = player.getUniqueId(); + Map savedBlocks = this.originalBlocks.computeIfAbsent(playerId, key -> new ConcurrentHashMap<>()); - return chunksToProcess.entrySet().stream() - .flatMap(entry -> getPointsWithoutAirOnChunk(player, entry)) + return this.groupByChunk(points).entrySet().stream() + .flatMap(entry -> this.filterChunkPoints(player, entry, savedBlocks)) .collect(Collectors.toSet()); } - private Stream getPointsWithoutAirOnChunk(Player player, Entry> entry) { - ChunkSnapshot snapshot = this.chunkCache.loadSnapshot(player, entry.getKey()); + private Stream filterChunkPoints(Player player, Entry> entry, Map savedBlocks) { + ChunkSnapshot snapshot = this.chunkCache.loadSnapshot(player, entry.getKey()); + if (snapshot == null) { return Stream.empty(); } - return entry.getValue().stream() - .filter(point -> isAir(entry.getKey(), point, snapshot)); + ChunkLocation chunk = entry.getKey(); + + return entry.getValue().stream().filter(point -> this.trySavePassableBlock(point, chunk, snapshot, savedBlocks)); } - private static boolean isAir(ChunkLocation location, BorderPoint point, ChunkSnapshot chunk) { - int xInsideChunk = point.x() - (location.x() << MINECRAFT_CHUNK_SHIFT); - int zInsideChunk = point.z() - (location.z() << MINECRAFT_CHUNK_SHIFT); - Material material = chunk.getBlockType(xInsideChunk, point.y(), zInsideChunk); + private boolean trySavePassableBlock(BorderPoint point, ChunkLocation chunk, ChunkSnapshot snapshot, Map savedBlocks) { + if (savedBlocks.containsKey(point)) { + return true; + } + + int localX = point.x() - (chunk.x() << MINECRAFT_CHUNK_SHIFT); + int localZ = point.z() - (chunk.z() << MINECRAFT_CHUNK_SHIFT); + + Material material = snapshot.getBlockType(localX, point.y(), localZ); - return material.isAir(); + if (material.isSolid()) { + return false; + } + + savedBlocks.put(point, this.resolveBlockId(material)); + + return true; } - private Map> splitByChunks(Collection blocks) { - return blocks.stream().collect(Collectors.groupingBy( - block -> new Vector3i( - block.x() >> MINECRAFT_CHUNK_SHIFT, - block.y() >> MINECRAFT_CHUNK_SHIFT, - block.z() >> MINECRAFT_CHUNK_SHIFT - ), + private int resolveBlockId(Material material) { + StateType stateType = StateTypes.getByName(material.getKey().toString()); + + if (stateType == null) { + return AIR_ID; + } + + return WrappedBlockState.getDefaultState(SERVER_VERSION, stateType).getGlobalId(); + } + + private EncodedBlock toBorderBlock(BorderPoint point) { + StateType type = this.settings.get().type.getStateType(point); + int blockId = WrappedBlockState.getDefaultState(SERVER_VERSION, type).getGlobalId(); + + return new EncodedBlock(blockId, point.x(), point.y(), point.z()); + } + + private EncodedBlock toOriginalBlock(BorderPoint point, Map savedBlocks) { + int blockId = savedBlocks != null ? savedBlocks.getOrDefault(point, AIR_ID) : AIR_ID; + + return new EncodedBlock(blockId, point.x(), point.y(), point.z()); + } + + private Map> groupByChunk(Collection points) { + return points.stream().collect(Collectors.groupingBy( + point -> new ChunkLocation(point.x() >> MINECRAFT_CHUNK_SHIFT, point.z() >> MINECRAFT_CHUNK_SHIFT), Collectors.toSet() )); } - private EncodedBlock toEncodedBlock(BorderPoint point) { - StateType type = settings.get().type.getStateType(point); - WrappedBlockState state = WrappedBlockState.getDefaultState(SERVER_VERSION, type); - return new EncodedBlock(state.getGlobalId(), point.x(), point.y(), point.z()); + private Map> splitByChunkSection(Collection points) { + return points.stream().collect(Collectors.groupingBy( + point -> new Vector3i(point.x() >> MINECRAFT_CHUNK_SHIFT, point.y() >> MINECRAFT_CHUNK_SHIFT, point.z() >> MINECRAFT_CHUNK_SHIFT), + Collectors.toSet() + )); } + private Object getLock(UUID uuid) { + return this.lockedPlayers.computeIfAbsent(uuid, k -> new Object()); + } }