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