Improved memory optimization of the explosion utils

This commit is contained in:
wolf
2025-06-24 17:29:00 -04:00
parent 49113e9660
commit f08f7bae77
16 changed files with 737 additions and 253 deletions

View File

@@ -3,13 +3,12 @@ package me.trouper.alias.server.systems;
import me.trouper.alias.server.Main; import me.trouper.alias.server.Main;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import java.util.Collections; import java.io.Closeable;
import java.util.HashSet; import java.util.*;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
public class TaskManager implements Main { public class TaskManager implements Closeable, Main {
private final ConcurrentHashMap<Integer, Boolean> tasks = new ConcurrentHashMap<>(); private final Map<Integer, Boolean> tasks = new HashMap<>();
private volatile boolean closed = false; private volatile boolean closed = false;
public int scheduleTask(Runnable task, long delay) { public int scheduleTask(Runnable task, long delay) {
@@ -22,7 +21,7 @@ public class TaskManager implements Main {
}, delay).getTaskId(); }, delay).getTaskId();
if (!closed) { if (!closed) {
tasks.put(taskId, Boolean.TRUE); tasks.put(taskId,Boolean.TRUE);
return taskId; return taskId;
} else { } else {
Bukkit.getScheduler().cancelTask(taskId); Bukkit.getScheduler().cancelTask(taskId);
@@ -30,6 +29,7 @@ public class TaskManager implements Main {
} }
} }
@Override
public void close() { public void close() {
closed = true; closed = true;
tasks.keySet().forEach(Bukkit.getScheduler()::cancelTask); tasks.keySet().forEach(Bukkit.getScheduler()::cancelTask);

View File

@@ -3,7 +3,9 @@ package me.trouper.alias.server.systems;
import me.trouper.alias.server.Main; import me.trouper.alias.server.Main;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.util.Vector;
public class Verbose implements Main { public class Verbose implements Main {
@@ -26,12 +28,18 @@ public class Verbose implements Main {
if (className.contains("-")) callerInfo = "Protected"; if (className.contains("-")) callerInfo = "Protected";
else callerInfo = className + "." + caller.getMethodName(); else callerInfo = className + "." + caller.getMethodName();
if (main.getCommon().getDebuggerExclusions().contains(callerInfo)) return; if (main.getCommon().getDebuggerExclusions().contains(callerInfo)) return;
} }
Component message = Text.format(Text.Pallet.INFO,verbose,args); Object[] processedArgs = processArgs(args);
message = Text.format(Text.Pallet.INFO,Component.text("{0} [DEBUG ^ {1}] [{2}] » {3}"),Component.text(main.getCommon().getPluginName()), Component.text(backtrace), Component.text(callerInfo), message); Component message = Text.format(Text.Pallet.INFO, verbose, processedArgs);
message = Text.format(Text.Pallet.INFO,
Component.text("{0} [DEBUG ^ {1}] [{2}] » {3}"),
Component.text(main.getCommon().getPluginName()),
Component.text(backtrace),
Component.text(callerInfo),
message
);
main.getPlugin().getComponentLogger().info(message); main.getPlugin().getComponentLogger().info(message);
@@ -49,4 +57,24 @@ public class Verbose implements Main {
public static void send(String verbose, Object... args) { public static void send(String verbose, Object... args) {
send(1,verbose,args); send(1,verbose,args);
} }
private static Object[] processArgs(Object... args) {
Object[] processed = new Object[args.length];
for (int i = 0; i < args.length; i++) {
Object arg = args[i];
if (arg instanceof Location loc) {
processed[i] = String.format("(%s, %.2f, %.2f, %.2f)",
loc.getWorld() != null ? loc.getWorld().getName() : "null",
loc.getX(), loc.getY(), loc.getZ());
} else if (arg instanceof Vector vec) {
processed[i] = String.format("(%.2f, %.2f, %.2f)",
vec.getX(), vec.getY(), vec.getZ());
} else {
processed[i] = arg;
}
}
return processed;
}
} }

View File

@@ -2,7 +2,6 @@ package me.trouper.alias.server.systems.burning;
import me.trouper.alias.server.Main; import me.trouper.alias.server.Main;
import me.trouper.alias.server.systems.TaskManager; import me.trouper.alias.server.systems.TaskManager;
import org.bukkit.Bukkit;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.block.Block; import org.bukkit.block.Block;
import org.bukkit.block.BlockFace; import org.bukkit.block.BlockFace;
@@ -27,12 +26,6 @@ public class BlockBurner implements Closeable, Main {
this.taskManager = new TaskManager(); this.taskManager = new TaskManager();
} }
public BlockBurner(BurnOptions options, TaskManager sharedTaskManager) {
this.options = options;
this.palette = new BurnPalette();
this.taskManager = sharedTaskManager;
}
@Override @Override
public void close() { public void close() {
taskManager.close(); taskManager.close();
@@ -40,8 +33,8 @@ public class BlockBurner implements Closeable, Main {
burning.clear(); burning.clear();
} }
public void burn(Block block, float heat) { public void burn(Block block, float heat, boolean checkOcclusion) {
if (isOccluded(block)) return; if (checkOcclusion && isOccluded(block)) return;
if (visited.contains(block)) return; if (visited.contains(block)) return;
visited.add(block); visited.add(block);
@@ -77,7 +70,7 @@ public class BlockBurner implements Closeable, Main {
long totalDelay = 0; long totalDelay = 0;
for (BurnStage stage : stages) { for (BurnStage stage : stages) {
totalDelay += stage.getDelay(); totalDelay += stage.getDelayTicks();
taskManager.scheduleTask(() -> { taskManager.scheduleTask(() -> {
if (block.getType().isAir()) return; if (block.getType().isAir()) return;

View File

@@ -2,7 +2,7 @@ package me.trouper.alias.server.systems.burning;
public class BurnOptions { public class BurnOptions {
private boolean disabled = false; private boolean disabled = false;
private double setFireChance = 1.0 / 80; private double setFireChance = 0.2;
public boolean isDisabled() { return disabled; } public boolean isDisabled() { return disabled; }
public void setDisabled(boolean disabled) { this.disabled = disabled; } public void setDisabled(boolean disabled) { this.disabled = disabled; }

View File

@@ -3,14 +3,14 @@ package me.trouper.alias.server.systems.burning;
import org.bukkit.block.data.BlockData; import org.bukkit.block.data.BlockData;
public class BurnStage { public class BurnStage {
private final long delay; private final long delayTicks;
private final BlockData blockData; private final BlockData blockData;
public BurnStage(long delay, BlockData blockData) { public BurnStage(long delayTicks, BlockData blockData) {
this.delay = delay; this.delayTicks = delayTicks;
this.blockData = blockData; this.blockData = blockData;
} }
public long getDelay() { return delay; } public long getDelayTicks() { return delayTicks; }
public BlockData getBlockData() { return blockData; } public BlockData getBlockData() { return blockData; }
} }

View File

@@ -77,17 +77,19 @@ public class CustomDisplayRaytracer {
return point -> HIT_BLOCK.test(point) && !point.getNearbyEntities(null, 5, true, 0.1, e -> e instanceof LivingEntity le && !le.isDead() && condition.test(e)).isEmpty(); return point -> HIT_BLOCK.test(point) && !point.getNearbyEntities(null, 5, true, 0.1, e -> e instanceof LivingEntity le && !le.isDead() && condition.test(e)).isEmpty();
} }
public static Point trace(Location start, Location end, Predicate<Point> hitCondition) { public static Point trace(Location start, Location end, Predicate<Point> hitCondition) {
return trace(start, end, 0.5, hitCondition); return trace(start, end, 0.5, hitCondition);
} }
public static Point trace(Location start, Location end, double interval, Predicate<Point> hitCondition) { public static Point trace(Location start, Location end, double interval, Predicate<Point> hitCondition) {
return trace(start, end.toVector().subtract(start.toVector()), end.distance(start), interval, hitCondition); Vector direction = end.toVector().subtract(start.toVector()).normalize();
double distance = end.distance(start);
return trace(start, direction, distance, interval, hitCondition);
} }
public static Point trace(Location start, Vector direction, double distance, Predicate<Point> hitCondition) { public static Point trace(Location start, Vector direction, double distance, Predicate<Point> hitCondition) {
return trace(start, direction, distance, 0.5, hitCondition); Vector normal = direction.clone().normalize();
return trace(start, normal, distance, 0.5, hitCondition);
} }
public static Point trace(Location start, Vector direction, double distance, double interval, Predicate<Point> hitCondition) { public static Point trace(Location start, Vector direction, double distance, double interval, Predicate<Point> hitCondition) {
@@ -285,7 +287,8 @@ public class CustomDisplayRaytracer {
} }
public static Point blocksInFrontOf(Location loc, Vector dir, double blocks, boolean missed) { public static Point blocksInFrontOf(Location loc, Vector dir, double blocks, boolean missed) {
return new Point(loc.clone().add(dir.getX() * blocks, dir.getY() * blocks, dir.getZ() * blocks), blocks, missed); Vector normal = dir.clone().normalize();
return new Point(loc.clone().add(normal.getX() * blocks, normal.getY() * blocks, normal.getZ() * blocks), blocks, missed);
} }
public static Vector offsetVector(Vector original, double angleDegrees) { public static Vector offsetVector(Vector original, double angleDegrees) {

View File

@@ -27,11 +27,6 @@ public class DisplayUtils implements Main {
return l -> l.getWorld().spawnParticle(Particle.DUST, l, 1, 0, 0, 0, 0, dust); return l -> l.getWorld().spawnParticle(Particle.DUST, l, 1, 0, 0, 0, 0, dust);
}; };
public static final Function<Boolean, Consumer<Location>> FLAME_PARTICLE_FACTORY = soul -> {
Particle flame = soul ? Particle.SOUL_FIRE_FLAME : Particle.FLAME;
return l -> l.getWorld().spawnParticle(flame, l, 1, 0, 0, 0, 0);
};
public static void ring(Location loc, double radius, Color color, float thickness) { public static void ring(Location loc, double radius, Color color, float thickness) {
ring(loc, radius, DUST_PARTICLE_FACTORY.apply(color, thickness)); ring(loc, radius, DUST_PARTICLE_FACTORY.apply(color, thickness));
} }

View File

@@ -0,0 +1,89 @@
package me.trouper.alias.server.systems.visual;
import org.bukkit.*;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Player;
import java.util.HashSet;
import java.util.Set;
public class ParticleUtils {
public static ParticleBuilder builder() {
return new ParticleBuilder();
}
public static class ParticleBuilder {
private Particle type = Particle.FLAME;
private int count = 1;
private double offsetX = 0;
private double offsetY = 0;
private double offsetZ = 0;
private float speed = 0.0f;
private Object data = null;
private Set<Player> viewers = new HashSet<>();
public ParticleBuilder type(Particle type) {
this.type = type;
return this;
}
public ParticleBuilder count(int count) {
this.count = count;
return this;
}
public ParticleBuilder offset(double x, double y, double z) {
this.offsetX = x;
this.offsetY = y;
this.offsetZ = z;
return this;
}
public ParticleBuilder speed(float speed) {
this.speed = speed;
return this;
}
public ParticleBuilder data(Particle.DustOptions dust) {
this.data = dust;
return this;
}
public ParticleBuilder data(Material block) {
this.data = block.createBlockData();
return this;
}
public ParticleBuilder data(BlockData blockData) {
this.data = blockData;
return this;
}
public ParticleBuilder data(Object customData) {
this.data = customData;
return this;
}
public ParticleBuilder viewers(Set<Player> viewers) {
if (viewers != null) {
this.viewers = viewers;
}
return this;
}
public void spawn(Location location) {
if (location == null || location.getWorld() == null) return;
if (viewers == null || viewers.isEmpty()) {
location.getWorld().spawnParticle(type, location, count, offsetX, offsetY, offsetZ, speed, data);
} else {
for (Player player : viewers) {
if (player != null && player.isOnline()) {
player.spawnParticle(type, location, count, offsetX, offsetY, offsetZ, speed, data);
}
}
}
}
}
}

View File

@@ -0,0 +1,112 @@
package me.trouper.alias.server.systems.world;
import org.bukkit.block.Block;
import java.util.*;
import java.util.concurrent.locks.ReentrantLock;
public class BlockHistory {
private final List<State> history = new ArrayList<>();
private final ReentrantLock lock = new ReentrantLock();
private int currentIndex = -1;
public void addChange(State state) {
lock.lock();
try {
if (currentIndex < history.size() - 1) {
history.subList(currentIndex + 1, history.size()).clear();
}
history.add(state);
currentIndex = history.size() - 1;
} finally {
lock.unlock();
}
}
public void addChange(Collection<Block> blocks) {
lock.lock();
try {
if (currentIndex < history.size() - 1) {
history.subList(currentIndex + 1, history.size()).clear();
}
State newState = new State(blocks);
history.add(newState);
currentIndex = history.size() - 1;
} finally {
lock.unlock();
}
}
public boolean canUndo() {
lock.lock();
try {
return currentIndex > 0;
} finally {
lock.unlock();
}
}
public boolean canRedo() {
lock.lock();
try {
return currentIndex < history.size() - 1;
} finally {
lock.unlock();
}
}
public boolean undo() {
lock.lock();
try {
if (!canUndo()) return false;
currentIndex--;
history.get(currentIndex).restore();
return true;
} finally {
lock.unlock();
}
}
public boolean redo() {
lock.lock();
try {
if (!canRedo()) return false;
currentIndex++;
history.get(currentIndex).restore();
return true;
} finally {
lock.unlock();
}
}
public int getCurrentIndex() {
lock.lock();
try {
return currentIndex;
} finally {
lock.unlock();
}
}
public int getHistorySize() {
lock.lock();
try {
return history.size();
} finally {
lock.unlock();
}
}
public Set<Block> getCurrentBlocks() {
lock.lock();
try {
if (currentIndex >= 0 && currentIndex < history.size()) {
return history.get(currentIndex).getBlocks();
}
return Collections.emptySet();
} finally {
lock.unlock();
}
}
}

View File

@@ -0,0 +1,50 @@
package me.trouper.alias.server.systems.world;
import me.trouper.alias.server.systems.burning.BurnOptions;
public class ExplosionOptions {
private double coreRadius = 3.0;
private double falloffRadius = 8.0;
private double maxBurnRadius = 15.0;
private double baseDamage = 20;
private double destructionDelay = 0.0; // SECONDS
private double burnDelay = 0.5; // SECONDS
private double maxHeat = 1.0;
private double minHeat = 0.1;
private boolean createParticles = true;
private boolean playSound = true;
private BurnOptions burnOptions = new BurnOptions();
public double getCoreRadius() { return coreRadius; }
public ExplosionOptions setCoreRadius(double coreRadius) { this.coreRadius = coreRadius; return this; }
public double getFalloffRadius() { return falloffRadius; }
public ExplosionOptions setFalloffRadius(double falloffRadius) { this.falloffRadius = falloffRadius; return this; }
public double getMaxBurnRadius() { return maxBurnRadius; }
public ExplosionOptions setMaxBurnRadius(double maxBurnRadius) { this.maxBurnRadius = maxBurnRadius; return this; }
public double getBaseDamage() { return baseDamage; }
public ExplosionOptions setBaseDamage(double baseDamage) { this.baseDamage = baseDamage; return this; }
public double getDestructionDelay() { return destructionDelay; }
public ExplosionOptions setDestructionDelay(double destructionDelay) { this.destructionDelay = destructionDelay; return this; }
public double getBurnDelay() { return burnDelay; }
public ExplosionOptions setBurnDelay(double burnDelay) { this.burnDelay = burnDelay; return this; }
public double getMaxHeat() { return maxHeat; }
public ExplosionOptions setMaxHeat(double maxHeat) { this.maxHeat = maxHeat; return this; }
public double getMinHeat() { return minHeat; }
public ExplosionOptions setMinHeat(double minHeat) { this.minHeat = minHeat; return this; }
public boolean isCreateParticles() { return createParticles; }
public ExplosionOptions setCreateParticles(boolean createParticles) { this.createParticles = createParticles; return this; }
public boolean isPlaySound() { return playSound; }
public ExplosionOptions setPlaySound(boolean playSound) { this.playSound = playSound; return this; }
public BurnOptions getBurnOptions() { return burnOptions; }
public ExplosionOptions setBurnOptions(BurnOptions burnOptions) { this.burnOptions = burnOptions; return this; }
}

View File

@@ -0,0 +1,67 @@
package me.trouper.alias.server.systems.world;
import me.trouper.alias.server.Main;
import me.trouper.alias.server.systems.TaskManager;
import org.bukkit.block.Block;
import java.io.Closeable;
import java.util.Set;
public class ExplosionResult implements Closeable, Main {
private State previousState;
private TaskManager taskManager;
private volatile boolean closed = false;
public ExplosionResult(Set<Block> affectedBlocks, TaskManager manager) {
this.taskManager = manager;
this.previousState = new State(affectedBlocks);
}
@Override
public void close() {
if (closed) return;
synchronized (this) {
if (closed) return;
closed = true;
if (taskManager != null) {
taskManager.close();
taskManager = null;
}
if (previousState != null) {
previousState.close();
previousState = null;
}
}
}
public void restore() {
if (closed) {
throw new IllegalStateException("ExplosionResult has been closed");
}
try {
if (previousState != null) {
previousState.restore();
}
} finally {
close();
}
}
public State getPreviousState() {
if (closed) return null;
return previousState;
}
public TaskManager getTaskManager() {
if (closed) return null;
return taskManager;
}
public boolean isClosed() {
return closed;
}
}

View File

@@ -1,156 +1,36 @@
package me.trouper.alias.server.systems.world; package me.trouper.alias.server.systems.world;
import me.trouper.alias.server.Main; import me.trouper.alias.server.Main;
import me.trouper.alias.server.systems.Verbose;
import me.trouper.alias.server.systems.TaskManager; import me.trouper.alias.server.systems.TaskManager;
import me.trouper.alias.server.systems.burning.BlockBurner; import me.trouper.alias.server.systems.burning.BlockBurner;
import me.trouper.alias.server.systems.burning.BurnOptions; import me.trouper.alias.utils.SoundPlayer;
import me.trouper.alias.utils.TargetingUtils;
import org.bukkit.*; import org.bukkit.*;
import org.bukkit.block.Block; import org.bukkit.block.Block;
import org.bukkit.block.BlockState;
import org.bukkit.entity.LivingEntity; import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryHolder;
import org.bukkit.inventory.ItemStack;
import org.bukkit.util.Vector; import org.bukkit.util.Vector;
import java.io.Closeable;
import java.util.*; import java.util.*;
import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.ThreadLocalRandom;
public class ExplosionUtils implements Main { public class ExplosionUtils implements Main {
public static class ExplosionOptions {
private double coreRadius = 3.0;
private double falloffRadius = 8.0;
private double maxBurnRadius = 15.0;
private double baseDamage = 20;
private double destructionDelay = 0.0; // SECONDS
private double burnDelay = 0.5; // SECONDS
private double maxHeat = 1.0;
private double minHeat = 0.1;
private boolean createParticles = true;
private boolean playSound = true;
private BurnOptions burnOptions = new BurnOptions();
public double getCoreRadius() { return coreRadius; }
public void setCoreRadius(double coreRadius) { this.coreRadius = coreRadius; }
public double getFalloffRadius() { return falloffRadius; }
public void setFalloffRadius(double falloffRadius) { this.falloffRadius = falloffRadius; }
public double getMaxBurnRadius() { return maxBurnRadius; }
public void setMaxBurnRadius(double maxBurnRadius) { this.maxBurnRadius = maxBurnRadius; }
public double getBaseDamage() { return baseDamage; }
public void setBaseDamage(double baseDamage) { this.baseDamage = baseDamage; }
public double getDestructionDelay() { return destructionDelay; }
public void setDestructionDelay(double destructionDelay) { this.destructionDelay = destructionDelay; }
public double getBurnDelay() { return burnDelay; }
public void setBurnDelay(double burnDelay) { this.burnDelay = burnDelay; }
public double getMaxHeat() { return maxHeat; }
public void setMaxHeat(double maxHeat) { this.maxHeat = maxHeat; }
public double getMinHeat() { return minHeat; }
public void setMinHeat(double minHeat) { this.minHeat = minHeat; }
public boolean isCreateParticles() { return createParticles; }
public void setCreateParticles(boolean createParticles) { this.createParticles = createParticles; }
public boolean isPlaySound() { return playSound; }
public void setPlaySound(boolean playSound) { this.playSound = playSound; }
public BurnOptions getBurnOptions() { return burnOptions; }
public void setBurnOptions(BurnOptions burnOptions) { this.burnOptions = burnOptions; }
}
public static class ExplosionResult {
private final Map<Block, BlockState> originalStates = new HashMap<>();
private final Map<Block, ItemStack[]> originalInventories = new HashMap<>();
private final TaskManager taskManager;
private final BlockBurner burner;
public ExplosionResult(TaskManager taskManager) {
this.taskManager = taskManager;
this.burner = new BlockBurner(new BurnOptions(), taskManager);
}
public ExplosionResult(BlockBurner burner) {
this.burner = burner;
this.taskManager = burner.getTaskManager();
}
public void cleanup() {
taskManager.close();
}
void recordSnapshot(Block block) {
BlockState state = block.getState();
originalStates.put(block, state);
if (state instanceof InventoryHolder) {
Inventory inv = ((InventoryHolder) state).getInventory();
originalInventories.put(block, inv.getContents());
}
}
public void restore() {
cleanup();
for (Map.Entry<Block, BlockState> entry : originalStates.entrySet()) {
Block block = entry.getKey();
BlockState snapshot = entry.getValue();
Bukkit.getScheduler().runTask(main.getPlugin(),()->{
block.setBlockData(snapshot.getBlockData(), false);
snapshot.update(true, false);
ItemStack[] contents = originalInventories.get(block);
if (contents != null && block.getState() instanceof InventoryHolder) {
Inventory inv = ((InventoryHolder) block.getState()).getInventory();
inv.setContents(contents);
}
});
}
}
public BlockBurner getBurner() { return burner; }
public TaskManager getTaskManager() { return taskManager; }
public Map<Block, BlockState> getOriginalStates() {
return originalStates;
}
public Map<Block, ItemStack[]> getOriginalInventories() {
return originalInventories;
}
}
public static ExplosionResult createExplosion(Location center, ExplosionOptions options) { public static ExplosionResult createExplosion(Location center, ExplosionOptions options) {
World world = center.getWorld(); World world = center.getWorld();
if (world == null) throw new IllegalArgumentException("Center location must have a valid world"); if (world == null) throw new IllegalArgumentException("Center location must have a valid world");
double maxBurnRadius = options.getMaxBurnRadius(); double maxBurnRadius = options.getMaxBurnRadius();
try {
Map<Block, Double> affectedBlocks = getBlocksInRadius(center, maxBurnRadius); Map<Block, Double> affectedBlocks = getBlocksInRadius(center, maxBurnRadius);
Map<UUID, Double> affectedEntities = getEntitiesInRadius(center, maxBurnRadius); Map<UUID, Double> affectedEntities = getEntitiesInRadius(center, maxBurnRadius);
TaskManager sharedTaskManager = new TaskManager(); BlockBurner burner = new BlockBurner(options.getBurnOptions());
BlockBurner burner = new BlockBurner(options.getBurnOptions(), sharedTaskManager); TaskManager sharedTaskManager = burner.getTaskManager();
ExplosionResult result = new ExplosionResult(burner); ExplosionResult result = new ExplosionResult(affectedBlocks.keySet(), sharedTaskManager);
for (Block block : affectedBlocks.keySet()) { List<Block> blocksToDestroy = new ArrayList<>();
if (block.getType().isAir()) continue; List<Block> blocksToBurn = new ArrayList<>();
result.recordSnapshot(block); Map<Block, Float> blocksHeatMap = new HashMap<>(affectedBlocks.size());
}
Set<Block> blocksToDestroy = new HashSet<>();
Set<Block> blocksToBurn = new HashSet<>();
Map<Block, Float> blocksHeatMap = new HashMap<>();
categorizeBlocks(affectedBlocks, options, blocksToDestroy, blocksToBurn, blocksHeatMap); categorizeBlocks(affectedBlocks, options, blocksToDestroy, blocksToBurn, blocksHeatMap);
@@ -158,29 +38,41 @@ public class ExplosionUtils implements Main {
scheduleBurning(blocksToBurn, blocksHeatMap, burner, center, options, sharedTaskManager); scheduleBurning(blocksToBurn, blocksHeatMap, burner, center, options, sharedTaskManager);
scheduleDamage(affectedEntities, options, sharedTaskManager); scheduleDamage(affectedEntities, options, sharedTaskManager);
if (options.isCreateParticles() || options.isPlaySound()) createExplosionEffects(center, options); if (options.isCreateParticles() || options.isPlaySound()) {
createExplosionEffects(center, options);
}
return result; return result;
} catch (Exception e) {
System.err.println("Failed to create explosion: " + e.getMessage());
throw e;
}
} }
private static Map<Block, Double> getBlocksInRadius(Location center, double radius) { private static Map<Block, Double> getBlocksInRadius(Location center, double radius) {
Map<Block, Double> blocks = new HashMap<>();
World world = center.getWorld(); World world = center.getWorld();
if (world == null) return Collections.emptyMap();
int radiusInt = (int) Math.ceil(radius); int radiusInt = (int) Math.ceil(radius);
Vector centerVec = center.toVector();
int estimatedBlocks = (int) ((4.0 / 3.0) * Math.PI * Math.pow(radius, 3));
Map<Block, Double> blocks = new HashMap<>(estimatedBlocks * 4 / 3 + 1);
int centerX = center.getBlockX();
int centerY = center.getBlockY();
int centerZ = center.getBlockZ();
double radiusSquared = radius * radius;
for (int x = -radiusInt; x <= radiusInt; x++) { for (int x = -radiusInt; x <= radiusInt; x++) {
for (int y = -radiusInt; y <= radiusInt; y++) { for (int y = -radiusInt; y <= radiusInt; y++) {
for (int z = -radiusInt; z <= radiusInt; z++) { for (int z = -radiusInt; z <= radiusInt; z++) {
Block block = world.getBlockAt( double distanceSquared = x*x + y*y + z*z;
center.getBlockX() + x, if (distanceSquared > radiusSquared) continue;
center.getBlockY() + y,
center.getBlockZ() + z
);
double distance = block.getLocation().toVector().distance(centerVec); Block block = world.getBlockAt(centerX + x, centerY + y, centerZ + z);
if (distance <= radius) { if (block != null && !block.getType().isAir()) {
double distance = Math.sqrt(distanceSquared);
blocks.put(block, distance); blocks.put(block, distance);
} }
} }
@@ -190,22 +82,33 @@ public class ExplosionUtils implements Main {
return blocks; return blocks;
} }
public static Map<UUID, Double> getEntitiesInRadius(Location center, double radius) { private static Map<UUID, Double> getEntitiesInRadius(Location center, double radius) {
List<LivingEntity> rawList = center.getNearbyLivingEntities(radius).stream().toList(); List<LivingEntity> rawList = center.getNearbyEntities(radius, radius, radius).stream()
Map<UUID, Double> entities = new HashMap<>(); .filter(entity -> entity instanceof LivingEntity)
.map(entity -> (LivingEntity) entity)
.toList();
if (rawList.isEmpty()) return Collections.emptyMap();
Map<UUID, Double> entities = new HashMap<>(rawList.size() * 4 / 3 + 1);
for (LivingEntity livingEntity : rawList) { for (LivingEntity livingEntity : rawList) {
entities.put(livingEntity.getUniqueId(),livingEntity.getLocation().distance(center)); if (livingEntity != null && livingEntity.isValid()) {
entities.put(livingEntity.getUniqueId(), livingEntity.getLocation().distance(center));
}
} }
return entities; return entities;
} }
private static void categorizeBlocks(Map<Block, Double> affectedBlocks, ExplosionOptions options, private static void categorizeBlocks(Map<Block, Double> affectedBlocks, ExplosionOptions options,
Set<Block> blocksToDestroy, Set<Block> blocksToBurn, List<Block> blocksToDestroy, List<Block> blocksToBurn,
Map<Block, Float> blocksHeatMap) { Map<Block, Float> blocksHeatMap) {
ThreadLocalRandom random = ThreadLocalRandom.current(); ThreadLocalRandom random = ThreadLocalRandom.current();
int totalBlocks = affectedBlocks.size();
List<Block> tempBlocksToBurn = new ArrayList<>(totalBlocks / 2);
for (Map.Entry<Block, Double> entry : affectedBlocks.entrySet()) { for (Map.Entry<Block, Double> entry : affectedBlocks.entrySet()) {
Block block = entry.getKey(); Block block = entry.getKey();
double distance = entry.getValue(); double distance = entry.getValue();
@@ -215,7 +118,11 @@ public class ExplosionUtils implements Main {
float heat = calculateHeat(distance, options); float heat = calculateHeat(distance, options);
blocksHeatMap.put(block, heat); blocksHeatMap.put(block, heat);
if (distance <= options.getCoreRadius()) blocksToDestroy.add(block); if (distance <= options.getCoreRadius()) {
blocksToDestroy.add(block);
tempBlocksToBurn.add(block);
continue;
}
if (distance <= options.getFalloffRadius()) { if (distance <= options.getFalloffRadius()) {
double destructionChance = 1.0 - ((distance - options.getCoreRadius()) / double destructionChance = 1.0 - ((distance - options.getCoreRadius()) /
@@ -223,14 +130,26 @@ public class ExplosionUtils implements Main {
destructionChance *= (0.7 + random.nextDouble() * 0.6); destructionChance *= (0.7 + random.nextDouble() * 0.6);
if (random.nextDouble() < destructionChance) blocksToDestroy.add(block); if (random.nextDouble() < destructionChance) {
else blocksToBurn.add(block); blocksToDestroy.add(block);
} else {
tempBlocksToBurn.add(block);
}
} else { } else {
double burnChance = 1.0 - ((distance - options.getFalloffRadius()) / double burnChance = 1.0 - ((distance - options.getFalloffRadius()) /
(options.getMaxBurnRadius() - options.getFalloffRadius())); (options.getMaxBurnRadius() - options.getFalloffRadius()));
burnChance *= (0.8 + random.nextDouble() * 0.6); burnChance *= (0.8 + random.nextDouble() * 0.6);
if (random.nextDouble() < burnChance) blocksToBurn.add(block); if (random.nextDouble() < burnChance) {
tempBlocksToBurn.add(block);
}
}
}
Set<Block> toDestroySet = new HashSet<>(blocksToDestroy);
for (Block block : tempBlocksToBurn) {
if (!isOccluded(block, toDestroySet)) {
blocksToBurn.add(block);
} }
} }
} }
@@ -238,42 +157,43 @@ public class ExplosionUtils implements Main {
private static float calculateHeat(double distance, ExplosionOptions options) { private static float calculateHeat(double distance, ExplosionOptions options) {
double normalizedDistance = distance / options.getMaxBurnRadius(); double normalizedDistance = distance / options.getMaxBurnRadius();
double heatRange = options.getMaxHeat() - options.getMinHeat(); double heatRange = options.getMaxHeat() - options.getMinHeat();
double heatFactor = Math.pow(1.0 - normalizedDistance, 2.0); double heatFactor = Math.pow(1.0 - normalizedDistance, 2.0);
return (float) (options.getMinHeat() + heatRange * heatFactor); return (float) (options.getMinHeat() + heatRange * heatFactor);
} }
private static void scheduleDestruction(Set<Block> blocksToDestroy, ExplosionOptions options, TaskManager taskManager) { private static void scheduleDestruction(List<Block> blocksToDestroy, ExplosionOptions options, TaskManager taskManager) {
if (blocksToDestroy.isEmpty()) return; if (blocksToDestroy.isEmpty()) return;
long destructionDelayTicks = (long) (options.getDestructionDelay() * 20); long destructionDelayTicks = (long) (options.getDestructionDelay() * 20);
taskManager.scheduleTask(() -> { taskManager.scheduleTask(() -> {
List<Block> blockList = new ArrayList<>(blocksToDestroy); Collections.shuffle(blocksToDestroy);
Collections.shuffle(blockList); int blocksPerWave = Math.max(1, blocksToDestroy.size() / 5);
int blocksPerWave = Math.max(1, blockList.size() / 5);
for (int wave = 0; wave < 5; wave++) { for (int wave = 0; wave < 5; wave++) {
int startIndex = wave * blocksPerWave; int startIndex = wave * blocksPerWave;
int endIndex = Math.min(startIndex + blocksPerWave, blockList.size()); int endIndex = Math.min(startIndex + blocksPerWave, blocksToDestroy.size());
if (startIndex >= blockList.size()) break; if (startIndex >= blocksToDestroy.size()) break;
final int finalStartIndex = startIndex;
final int finalEndIndex = endIndex;
taskManager.scheduleTask(() -> { taskManager.scheduleTask(() -> {
for (int i = startIndex; i < endIndex; i++) { for (int i = finalStartIndex; i < finalEndIndex; i++) {
Block block = blockList.get(i); if (i < blocksToDestroy.size()) {
if (!block.getType().isAir()) { Block block = blocksToDestroy.get(i);
if (block != null && !block.getType().isAir()) {
block.setType(Material.AIR); block.setType(Material.AIR);
} }
} }
}, wave * 2); }
}, wave * 2L);
} }
}, destructionDelayTicks); }, destructionDelayTicks);
} }
private static void scheduleBurning(Set<Block> blocksToMaybeBurn, Map<Block, Float> blocksHeatMap, private static void scheduleBurning(List<Block> blocksToMaybeBurn, Map<Block, Float> blocksHeatMap,
BlockBurner burner, Location center, ExplosionOptions options, BlockBurner burner, Location center, ExplosionOptions options,
TaskManager taskManager) { TaskManager taskManager) {
if (blocksToMaybeBurn.isEmpty()) return; if (blocksToMaybeBurn.isEmpty()) return;
@@ -281,34 +201,34 @@ public class ExplosionUtils implements Main {
long burnDelayTicks = (long) (options.getBurnDelay() * 20); long burnDelayTicks = (long) (options.getBurnDelay() * 20);
taskManager.scheduleTask(() -> { taskManager.scheduleTask(() -> {
List<Block> blockList = new ArrayList<>(blocksToMaybeBurn); Collections.shuffle(blocksToMaybeBurn);
Collections.shuffle(blockList);
Map<Integer, List<Block>> burnWaves = new HashMap<>(); Map<Integer, List<Block>> burnWaves = new HashMap<>();
for (Block block : blockList) { for (Block block : blocksToMaybeBurn) {
if (block != null) {
double distance = block.getLocation().distance(center); double distance = block.getLocation().distance(center);
int waveIndex = (int) (distance / 2.0); int waveIndex = (int) (distance / 2.0);
burnWaves.computeIfAbsent(waveIndex, k -> new ArrayList<>()).add(block); burnWaves.computeIfAbsent(waveIndex, k -> new ArrayList<>()).add(block);
} }
}
for (Map.Entry<Integer, List<Block>> waveEntry : burnWaves.entrySet()) { for (Map.Entry<Integer, List<Block>> waveEntry : burnWaves.entrySet()) {
int waveDelay = waveEntry.getKey() * 3; int waveDelay = waveEntry.getKey() * 3;
List<Block> waveBlocks = waveEntry.getValue(); List<Block> waveBlocks = waveEntry.getValue();
taskManager.scheduleTask(() -> { taskManager.scheduleTask(() -> {
ThreadLocalRandom random = ThreadLocalRandom.current();
for (Block block : waveBlocks) { for (Block block : waveBlocks) {
if (burner.isClosed()) continue; if (burner.isClosed() || block == null) continue;
float heat = blocksHeatMap.getOrDefault(block, 0.1f); float heat = blocksHeatMap.getOrDefault(block, 0.1f);
ThreadLocalRandom random = ThreadLocalRandom.current();
int randomDelay = random.nextInt(0, 10); int randomDelay = random.nextInt(0, 10);
taskManager.scheduleTask(() -> { taskManager.scheduleTask(() -> {
if (!burner.isClosed() && !block.getType().isAir()) { if (!burner.isClosed() && block != null && !block.getType().isAir()) {
burner.burn(block, heat); burner.burn(block, heat, false);
} }
}, randomDelay); }, randomDelay);
} }
@@ -318,36 +238,43 @@ public class ExplosionUtils implements Main {
} }
private static void scheduleDamage(Map<UUID, Double> affected, ExplosionOptions options, TaskManager taskManager) { private static void scheduleDamage(Map<UUID, Double> affected, ExplosionOptions options, TaskManager taskManager) {
if (affected.isEmpty()) return;
double baseDamage = options.getBaseDamage(); double baseDamage = options.getBaseDamage();
double igniteDistance = options.getMaxBurnRadius(); double igniteDistance = options.getMaxBurnRadius();
double halfDamageDistance = options.getFalloffRadius(); double halfDamageDistance = options.getFalloffRadius();
double fullDamageDistance = options.getCoreRadius(); double fullDamageDistance = options.getCoreRadius();
for (Map.Entry<UUID, Double> entityDistance : affected.entrySet()) { for (Map.Entry<UUID, Double> entityDistance : affected.entrySet()) {
LivingEntity liv = (LivingEntity) Bukkit.getEntity(entityDistance.getKey()); UUID entityId = entityDistance.getKey();
if (liv == null) continue; if (entityId == null) continue;
double distance = entityDistance.getValue(); double distance = entityDistance.getValue();
long delay = Math.max(1, (long) distance / 2);
if (distance >= halfDamageDistance && distance <= igniteDistance) { if (distance >= halfDamageDistance && distance <= igniteDistance) {
taskManager.scheduleTask(()->{ taskManager.scheduleTask(() -> {
liv.setFireTicks(5 * 20); LivingEntity entity = (LivingEntity) Bukkit.getEntity(entityId);
},(long) distance / 2); if (entity != null && entity.isValid()) {
return; entity.setFireTicks(5 * 20);
} }
if (distance >= fullDamageDistance && distance <= halfDamageDistance) { }, delay);
taskManager.scheduleTask(()->{ } else if (distance >= fullDamageDistance && distance <= halfDamageDistance) {
liv.setFireTicks(10 * 20); taskManager.scheduleTask(() -> {
liv.damage(baseDamage / 2); LivingEntity entity = (LivingEntity) Bukkit.getEntity(entityId);
},(long) distance / 2); if (entity != null && entity.isValid()) {
return; entity.setFireTicks(10 * 20);
entity.damage(baseDamage / 2);
} }
if (distance <= fullDamageDistance) { }, delay);
taskManager.scheduleTask(()->{ } else if (distance <= fullDamageDistance) {
liv.setFireTicks(15 * 20); taskManager.scheduleTask(() -> {
liv.damage(baseDamage); LivingEntity entity = (LivingEntity) Bukkit.getEntity(entityId);
},(long) distance / 2); if (entity != null && entity.isValid()) {
return; entity.setFireTicks(15 * 20);
entity.damage(baseDamage);
}
}, delay);
} }
} }
} }
@@ -356,9 +283,12 @@ public class ExplosionUtils implements Main {
World world = center.getWorld(); World world = center.getWorld();
if (world == null) return; if (world == null) return;
double soundRange = options.getMaxBurnRadius() * 20;
if (options.isPlaySound()) { if (options.isPlaySound()) {
world.playSound(center, Sound.ENTITY_GENERIC_EXPLODE, 1.0f, 0.8f); new SoundPlayer(Sound.ENTITY_WARDEN_SONIC_BOOM, 40, 0.5F).playAt(center, soundRange);
world.playSound(center, Sound.ENTITY_LIGHTNING_BOLT_THUNDER, 0.5f, 1.2f); new SoundPlayer(Sound.ENTITY_WARDEN_SONIC_BOOM, 40, 1.0F).playAt(center, soundRange);
new SoundPlayer(Sound.ITEM_TOTEM_USE, 40, 0.5F).playAt(center, soundRange);
} }
if (options.isCreateParticles()) { if (options.isCreateParticles()) {
@@ -368,22 +298,25 @@ public class ExplosionUtils implements Main {
world.spawnParticle(Particle.FLAME, center, 30, 3, 3, 3, 0.1); world.spawnParticle(Particle.FLAME, center, 30, 3, 3, 3, 0.1);
Bukkit.getScheduler().runTaskLater(main.getPlugin(), () -> { Bukkit.getScheduler().runTaskLater(main.getPlugin(), () -> {
world.spawnParticle(Particle.SMOKE, center, 50, 4, 4, 4, 0.02); if (center.getWorld() != null) {
}, 20); center.getWorld().spawnParticle(Particle.SMOKE, center, 50, 4, 4, 4, 0.02);
}
}, 20L);
} }
} }
public static ExplosionResult createExplosion(Location center) { private static boolean isOccluded(Block block, Set<Block> doesNotOcclude) {
return createExplosion(center, new ExplosionOptions()); if (block == null) return true;
return isOccluding(block.getRelative(0, 1, 0), doesNotOcclude) &&
isOccluding(block.getRelative(0, -1, 0), doesNotOcclude) &&
isOccluding(block.getRelative(1, 0, 0), doesNotOcclude) &&
isOccluding(block.getRelative(-1, 0, 0), doesNotOcclude) &&
isOccluding(block.getRelative(0, 0, 1), doesNotOcclude) &&
isOccluding(block.getRelative(0, 0, -1), doesNotOcclude);
} }
public static ExplosionResult createExplosion(Location center, double coreRadius, double falloffRadius, double maxBurnRadius) { private static boolean isOccluding(Block block, Set<Block> doesNotOcclude) {
ExplosionOptions options = new ExplosionOptions(); return block != null && block.getType().isOccluding() && !doesNotOcclude.contains(block);
options.setCoreRadius(coreRadius);
options.setFalloffRadius(falloffRadius);
options.setMaxBurnRadius(maxBurnRadius);
options.setBurnDelay(0);
options.setDestructionDelay(0);
return createExplosion(center, options);
} }
} }

View File

@@ -1,5 +1,85 @@
package me.trouper.alias.server.systems.world; package me.trouper.alias.server.systems.world;
public class Snapshot { import me.trouper.alias.server.Main;
import org.bukkit.Bukkit;
import org.bukkit.block.Block;
import org.bukkit.block.BlockState;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryHolder;
import org.bukkit.inventory.ItemStack;
import java.util.HashMap;
import java.util.Map;
public class Snapshot {
private final BlockState state;
private final Map<Integer, ItemStack> inventory;
public Snapshot(Block block) {
this.state = block.getState();
if (state instanceof InventoryHolder) {
Inventory inv = ((InventoryHolder) state).getInventory();
int size = inv.getSize();
int nonEmptySlots = 0;
for (int i = 0; i < size; i++) {
ItemStack item = inv.getItem(i);
if (item != null && !item.isEmpty()) {
nonEmptySlots++;
}
}
if (nonEmptySlots > 0) {
this.inventory = new HashMap<>(nonEmptySlots * 4 / 3 + 1);
for (int i = 0; i < size; i++) {
ItemStack item = inv.getItem(i);
if (item != null && !item.isEmpty()) {
inventory.put(i, item.clone());
}
}
} else {
this.inventory = null;
}
} else {
this.inventory = null;
}
}
public void restore(Block block) {
if (block == null || state == null) return;
Bukkit.getScheduler().runTask(Main.main.getPlugin(), () -> {
try {
block.setBlockData(state.getBlockData());
if (inventory != null && block.getState() instanceof InventoryHolder) {
InventoryHolder holder = (InventoryHolder) block.getState();
Inventory inv = holder.getInventory();
inv.clear();
for (Map.Entry<Integer, ItemStack> entry : inventory.entrySet()) {
int slot = entry.getKey();
ItemStack item = entry.getValue();
if (slot >= 0 && slot < inv.getSize() && item != null) {
inv.setItem(slot, item.clone());
}
}
}
} catch (Exception e) {
System.err.println("Failed to restore block at " + block.getLocation() + ": " + e.getMessage());
}
});
}
public BlockState getState() {
return state;
}
public Map<Integer, ItemStack> getInventory() {
return inventory;
}
} }

View File

@@ -0,0 +1,84 @@
package me.trouper.alias.server.systems.world;
import org.bukkit.Location;
import org.bukkit.block.Block;
import java.io.Closeable;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class State implements Closeable {
private Map<Location, Snapshot> snapshots;
private volatile boolean closed = false;
public State(Collection<Block> blocks) {
this.snapshots = new HashMap<>(blocks.size() * 4 / 3 + 1);
for (Block block : blocks) {
if (block != null && block.getWorld() != null) {
snapshots.put(block.getLocation(), new Snapshot(block));
}
}
}
public void restore() {
if (closed || snapshots == null) {
return;
}
try {
for (Map.Entry<Location, Snapshot> entry : snapshots.entrySet()) {
Location loc = entry.getKey();
Snapshot snapshot = entry.getValue();
if (loc != null && snapshot != null && loc.getWorld() != null) {
Block block = loc.getBlock();
if (block != null) {
snapshot.restore(block);
}
}
}
} finally {
close();
}
}
public Set<Block> getBlocks() {
if (closed || snapshots == null) {
return new HashSet<>();
}
Set<Block> blocks = new HashSet<>(snapshots.size());
for (Location loc : snapshots.keySet()) {
if (loc != null && loc.getWorld() != null) {
Block block = loc.getBlock();
if (block != null) {
blocks.add(block);
}
}
}
return blocks;
}
@Override
public void close() {
if (closed) return;
synchronized (this) {
if (closed) return;
closed = true;
if (snapshots != null) {
snapshots.clear();
snapshots = null;
}
}
}
public boolean isClosed() {
return closed;
}
}

View File

@@ -0,0 +1,21 @@
package me.trouper.alias.utils;
public class FormatUtils {
public static String formatEnum(Enum<?> obj) {
if (obj == null) return "Null";
String name = obj.name();
String[] words = name.toLowerCase().split("_");
StringBuilder formatted = new StringBuilder();
for (String word : words) {
if (!word.isEmpty()) {
formatted.append(Character.toUpperCase(word.charAt(0)))
.append(word.substring(1))
.append(" ");
}
}
return formatted.toString().trim();
}
}

View File

@@ -0,0 +1,29 @@
package me.trouper.alias.utils;
import org.bukkit.block.BlockState;
import org.bukkit.block.Container;
import org.bukkit.entity.Entity;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BlockStateMeta;
public final class InventoryUtils {
public static Inventory getInventory(Entity entity) {
if (entity instanceof org.bukkit.inventory.InventoryHolder inventoryHolder) {
return inventoryHolder.getInventory();
}
return null;
}
public static Inventory getInventory(ItemStack containerItem) {
if (containerItem.getItemMeta() instanceof BlockStateMeta blockStateMeta) {
BlockState blockState = blockStateMeta.getBlockState();
if (blockState instanceof Container container) {
return container.getInventory();
}
}
return null;
}
}