diff --git a/pom.xml b/pom.xml index 8999c769..272ff021 100644 --- a/pom.xml +++ b/pom.xml @@ -64,7 +64,7 @@ su.nightexpress.nightcore main - 2.10.0 + 2.16.3 provided diff --git a/src/main/java/su/nightexpress/excellentcrates/crate/CrateManager.java b/src/main/java/su/nightexpress/excellentcrates/crate/CrateManager.java index bc209b9f..fd20699e 100644 --- a/src/main/java/su/nightexpress/excellentcrates/crate/CrateManager.java +++ b/src/main/java/su/nightexpress/excellentcrates/crate/CrateManager.java @@ -96,11 +96,11 @@ protected void onLoad() { this.loadCrates(); this.loadUI(); this.loadDialogs(); - this.plugin.runTask(task -> this.reportProblems()); // After everything is loaded. + this.plugin.runTask(() -> this.reportProblems()); // After everything is loaded. this.addListener(new CrateListener(this.plugin, this)); - this.addAsyncTask(this::playCrateEffects, 1L); + this.addTask(this::playCrateEffects, 1L); this.addAsyncTask(this::saveCrates, Config.CRATE_SAVE_INTERVAL.get()); } @@ -743,8 +743,11 @@ public void playCrateEffects() { Location location = worldPos.toLocation(); if (location == null) return; - CrateUtils.getPlayersForEffects(location).forEach(player -> { - effect.playStep(location, particle, player); + // Dispatch onto the region/thread that owns this location before touching world/player state (Folia). + this.plugin.runTask(location, () -> { + CrateUtils.getPlayersForEffects(location).forEach(player -> { + effect.playStep(location, particle, player); + }); }); }); }); diff --git a/src/main/java/su/nightexpress/excellentcrates/crate/impl/Crate.java b/src/main/java/su/nightexpress/excellentcrates/crate/impl/Crate.java index 243062e4..83d16909 100644 --- a/src/main/java/su/nightexpress/excellentcrates/crate/impl/Crate.java +++ b/src/main/java/su/nightexpress/excellentcrates/crate/impl/Crate.java @@ -51,6 +51,7 @@ import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; @@ -106,7 +107,8 @@ public Crate(@NotNull CratesPlugin plugin, @NotNull Path path, @NotNull String i this.costMap = new LinkedHashMap<>(); this.rewardMap = new LinkedHashMap<>(); - this.blockPositions = new HashSet<>(); + // Air-block filtering on load() removes entries from a region thread per position (Folia), so this needs to be concurrent-safe. + this.blockPositions = ConcurrentHashMap.newKeySet(); this.milestones = new HashSet<>(); this.description = new ArrayList<>(); } @@ -219,11 +221,21 @@ private void load(@NotNull FileConfig config) throws IllegalStateException { this.addCost(cost); }); - this.blockPositions.addAll(config.getStringList("Block.Positions").stream().map(WorldPos::deserialize).toList()); + List positions = config.getStringList("Block.Positions").stream().map(WorldPos::deserialize).toList(); + this.blockPositions.addAll(positions); + if (!Config.isCrateInAirBlocksAllowed()) { - this.blockPositions.removeIf(pos -> { - Block block = pos.toBlock(); - return block != null && block.isEmpty(); + positions.forEach(pos -> { + Location location = pos.toLocation(); + if (location == null) return; + + // Dispatch onto the region/thread that owns this block before reading its state (Folia). + this.plugin.runTask(location, () -> { + Block block = pos.toBlock(); + if (block != null && block.isEmpty()) { + this.blockPositions.remove(pos); + } + }); }); } diff --git a/src/main/java/su/nightexpress/excellentcrates/data/DataManager.java b/src/main/java/su/nightexpress/excellentcrates/data/DataManager.java index 32cfdd1d..666dcb52 100644 --- a/src/main/java/su/nightexpress/excellentcrates/data/DataManager.java +++ b/src/main/java/su/nightexpress/excellentcrates/data/DataManager.java @@ -34,7 +34,7 @@ public DataManager(@NotNull CratesPlugin plugin) { @Override protected void onLoad() { - this.plugin.runTaskAsync(task -> this.loadData()); + this.plugin.runTaskAsync(() -> this.loadData()); this.addAsyncTask(this::saveCrateDatas, Config.DATA_CRATE_DATA_SAVE_INTERVAL.get()); this.addAsyncTask(this::saveRewardLimits, Config.DATA_REWARD_LIMITS_SAVE_INTERVAL.get()); @@ -150,13 +150,13 @@ public GlobalCrateData getCrateDataOrCreate(@NotNull Crate crate) { if (data != null) return data; GlobalCrateData fresh = GlobalCrateData.create(crate); - this.plugin.runTaskAsync(task -> this.plugin.getDataHandler().insertCrateData(fresh)); + this.plugin.runTaskAsync(() -> this.plugin.getDataHandler().insertCrateData(fresh)); this.crateDataMap.put(fresh.getCrateId(), fresh); return fresh; } public void deleteCrateData(@NotNull Crate crate) { - this.plugin.runTaskAsync(task -> this.plugin.getDataHandler().deleteCrateData(crate)); + this.plugin.runTaskAsync(() -> this.plugin.getDataHandler().deleteCrateData(crate)); this.crateDataMap.remove(crate.getId()); } @@ -168,7 +168,7 @@ public RewardData getRewardLimitOrCreate(@NotNull Reward reward, @Nullable Playe if (limit != null) return limit; RewardData fresh = RewardData.create(reward, player); - this.plugin.runTaskAsync(task -> this.plugin.getDataHandler().insertRewardLimit(fresh)); + this.plugin.runTaskAsync(() -> this.plugin.getDataHandler().insertRewardLimit(fresh)); this.addRewardLimit(fresh); return fresh; } @@ -190,14 +190,14 @@ private void addRewardLimit(@NotNull RewardData limit) { } public void deleteRewardLimit(@NotNull RewardData limit) { - this.plugin.runTaskAsync(task -> this.plugin.getDataHandler().deleteRewardLimit(limit)); + this.plugin.runTaskAsync(() -> this.plugin.getDataHandler().deleteRewardLimit(limit)); this.rewardLimitMap.remove(getRewardKey(limit)); } public void deleteRewardLimits(@NotNull Crate crate) { String crateId = crate.getId(); - this.plugin.runTaskAsync(task -> this.plugin.getDataHandler().deleteRewardLimits(crate)); + this.plugin.runTaskAsync(() -> this.plugin.getDataHandler().deleteRewardLimits(crate)); this.rewardLimitMap.keySet().removeIf(key -> key.crateId().equalsIgnoreCase(crateId)); } @@ -205,14 +205,14 @@ public void deleteRewardLimits(@NotNull Reward reward) { String crateId = reward.getCrate().getId(); String rewardId = reward.getId(); - this.plugin.runTaskAsync(task -> this.plugin.getDataHandler().deleteRewardLimits(reward)); + this.plugin.runTaskAsync(() -> this.plugin.getDataHandler().deleteRewardLimits(reward)); this.rewardLimitMap.keySet().removeIf(key -> key.crateId().equalsIgnoreCase(crateId) && key.rewardId().equalsIgnoreCase(rewardId)); } public void deleteRewardLimits(@NotNull UUID playerId) { String holder = playerId.toString(); - this.plugin.runTaskAsync(task -> this.plugin.getDataHandler().deleteRewardLimits(playerId)); + this.plugin.runTaskAsync(() -> this.plugin.getDataHandler().deleteRewardLimits(playerId)); this.rewardLimitMap.keySet().removeIf(key -> key.holder().equalsIgnoreCase(holder)); } diff --git a/src/main/java/su/nightexpress/excellentcrates/hologram/HologramManager.java b/src/main/java/su/nightexpress/excellentcrates/hologram/HologramManager.java index 78300140..11709aa5 100644 --- a/src/main/java/su/nightexpress/excellentcrates/hologram/HologramManager.java +++ b/src/main/java/su/nightexpress/excellentcrates/hologram/HologramManager.java @@ -24,6 +24,7 @@ import su.nightexpress.nightcore.util.placeholder.Replacer; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; public class HologramManager extends AbstractManager { @@ -33,7 +34,8 @@ public class HologramManager extends AbstractManager { public HologramManager(@NotNull CratesPlugin plugin) { super(plugin); - this.displayMap = new HashMap<>(); + // Creation/rendering is dispatched per block location (Folia regions), so this map can be written concurrently. + this.displayMap = new ConcurrentHashMap<>(); } @Override @@ -41,7 +43,7 @@ protected void onLoad() { if (this.detectHandler()) { this.addListener(new HologramListener(this.plugin, this)); - this.addAsyncTask(this::tickHolograms, Config.CRATE_HOLOGRAM_UPDATE_INTERVAL.get()); + this.addTask(this::tickHolograms, Config.CRATE_HOLOGRAM_UPDATE_INTERVAL.get()); } } @@ -160,42 +162,50 @@ public void render(@NotNull Crate crate) { if (group.isDisabled()) continue; WorldPos blockPosition = group.getBlockPosition(); - World world = blockPosition.getWorld(); - Location location = blockPosition.toLocation(); + Location dispatchLocation = blockPosition.toLocation(); + if (dispatchLocation == null) continue; - if (!blockPosition.isChunkLoaded() || world == null || location == null) { - this.discard(group); // Remove all viewers and send entity destroy packet. - continue; - } - - List players = new ArrayList<>(world.getPlayers()); - players.removeIf(player -> { - if (CrateUtils.isInEffectRange(player, location)) return false; + // Dispatch onto the region/thread that owns this block before touching world/player state (Folia). + this.plugin.runTask(dispatchLocation, () -> this.renderGroup(group, blockPosition, text)); + } + } - this.removeForViewer(player, group); - return true; - }); + private void renderGroup(@NotNull FakeEntityGroup group, @NotNull WorldPos blockPosition, @NotNull List text) { + World world = blockPosition.getWorld(); + Location location = blockPosition.toLocation(); - if (players.isEmpty()) { - this.discard(group); // Remove all viewers and send entity destroy packet. - continue; - } + if (!blockPosition.isChunkLoaded() || world == null || location == null) { + this.discard(group); // Remove all viewers and send entity destroy packet. + return; + } - players.forEach(player -> { - boolean needSpawn = !group.isViewer(player); + List players = new ArrayList<>(world.getPlayers()); + players.removeIf(player -> { + if (CrateUtils.isInEffectRange(player, location)) return false; - List hologramText = Replacer.create().replacePlaceholderAPI(player).apply(text); - List holograms = group.getEntities(); - for (int index = 0; index < holograms.size(); index++) { - // Fix for fake entity's text not being updated/replaced when text size is less than holograms amount, so force it to empty string. - String line = index >= hologramText.size() ? "" : hologramText.get(index); - FakeEntity entity = holograms.get(index); - this.handler.sendHologramPackets(player, entity, needSpawn, line); - } + this.removeForViewer(player, group); + return true; + }); - group.addViewer(player); - }); + if (players.isEmpty()) { + this.discard(group); // Remove all viewers and send entity destroy packet. + return; } + + players.forEach(player -> { + boolean needSpawn = !group.isViewer(player); + + List hologramText = Replacer.create().replacePlaceholderAPI(player).apply(text); + List holograms = group.getEntities(); + for (int index = 0; index < holograms.size(); index++) { + // Fix for fake entity's text not being updated/replaced when text size is less than holograms amount, so force it to empty string. + String line = index >= hologramText.size() ? "" : hologramText.get(index); + FakeEntity entity = holograms.get(index); + this.handler.sendHologramPackets(player, entity, needSpawn, line); + } + + group.addViewer(player); + }); } private void createIfAbsent(@NotNull Crate crate) { @@ -206,28 +216,34 @@ private void createIfAbsent(@NotNull Crate crate) { if (originText.isEmpty()) return; FakeDisplay display = new FakeDisplay(); + // Put it before dispatching, so the containsKey guard above prevents recreation while groups populate async. + this.displayMap.put(crate.getId(), display); double yOffset = crate.getHologramYOffset() + 0.2; double lineGap = Config.CRATE_HOLOGRAM_LINE_GAP.get(); crate.getBlockPositions().forEach(blockPos -> { - Block block = blockPos.toBlock(); - if (block == null) return; + Location blockLocation = blockPos.toLocation(); + if (blockLocation == null) return; - double height = block.getBoundingBox().getHeight() / 2D + yOffset; + // Dispatch onto the region/thread that owns this block before touching world state (Folia). + this.plugin.runTask(blockLocation, () -> { + Block block = blockPos.toBlock(); + if (block == null) return; - // Allocate ID values for our fake entities, so there is no clash with new server entities. + double height = block.getBoundingBox().getHeight() / 2D + yOffset; - FakeEntityGroup group = display.getGroupOrCreate(blockPos); + // Allocate ID values for our fake entities, so there is no clash with new server entities. - for (int index = 0; index < originText.size(); index++) { - double gap = lineGap * index; + FakeEntityGroup group = display.getGroupOrCreate(blockPos); - Location location = LocationUtil.setCenter3D(block.getLocation()).add(0, height + gap, 0); - group.addEntity(FakeEntity.create(location)); - } - }); + for (int index = 0; index < originText.size(); index++) { + double gap = lineGap * index; - this.displayMap.put(crate.getId(), display); + Location location = LocationUtil.setCenter3D(block.getLocation()).add(0, height + gap, 0); + group.addEntity(FakeEntity.create(location)); + } + }); + }); } } diff --git a/src/main/java/su/nightexpress/excellentcrates/hologram/entity/FakeDisplay.java b/src/main/java/su/nightexpress/excellentcrates/hologram/entity/FakeDisplay.java index bfb263f3..8114bc19 100644 --- a/src/main/java/su/nightexpress/excellentcrates/hologram/entity/FakeDisplay.java +++ b/src/main/java/su/nightexpress/excellentcrates/hologram/entity/FakeDisplay.java @@ -4,17 +4,18 @@ import org.jetbrains.annotations.Nullable; import su.nightexpress.excellentcrates.util.pos.WorldPos; -import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; public class FakeDisplay { private final Map entityGroups; public FakeDisplay() { - this.entityGroups = new HashMap<>(); + // Groups for different block placements are created/rendered on different region threads (Folia). + this.entityGroups = new ConcurrentHashMap<>(); } @NotNull diff --git a/src/main/java/su/nightexpress/excellentcrates/key/KeyManager.java b/src/main/java/su/nightexpress/excellentcrates/key/KeyManager.java index 3dc02645..ceb0a7c8 100644 --- a/src/main/java/su/nightexpress/excellentcrates/key/KeyManager.java +++ b/src/main/java/su/nightexpress/excellentcrates/key/KeyManager.java @@ -47,7 +47,7 @@ protected void onLoad() { this.loadCost(); this.loadKeys(); this.loadDialogs(); - this.plugin.runTask(task -> this.reportProblems()); // When everything is loaded. + this.plugin.runTask(() -> this.reportProblems()); // When everything is loaded. this.addListener(new KeyListener(this.plugin, this)); this.addAsyncTask(this::saveKeys, Config.CRATE_SAVE_INTERVAL.get()); // TODO Config diff --git a/src/main/java/su/nightexpress/excellentcrates/opening/OpeningManager.java b/src/main/java/su/nightexpress/excellentcrates/opening/OpeningManager.java index b44a4580..f2320547 100644 --- a/src/main/java/su/nightexpress/excellentcrates/opening/OpeningManager.java +++ b/src/main/java/su/nightexpress/excellentcrates/opening/OpeningManager.java @@ -152,7 +152,8 @@ public Opening getOpening(@NotNull Player player) { } public void tickOpenings() { - this.getOpenings().forEach(Opening::tick); + // Each opening owns a player's inventory/menu state, so tick it on that player's own thread (Folia). + this.getOpenings().forEach(opening -> this.plugin.runTask(opening.getPlayer(), opening::tick)); } public boolean isOpening(@NotNull Player player) { diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 64e88bf0..8bf7a7a5 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -8,4 +8,5 @@ softdepend: - PlaceholderAPI - ProtocolLib - packetevents -api-version: 1.21 \ No newline at end of file +api-version: 1.21 +folia-supported: true \ No newline at end of file