diff --git a/src/main/java/me/trouper/alias/server/systems/TaskManager.java b/src/main/java/me/trouper/alias/server/systems/TaskManager.java index f3f1cd9..dc2b076 100644 --- a/src/main/java/me/trouper/alias/server/systems/TaskManager.java +++ b/src/main/java/me/trouper/alias/server/systems/TaskManager.java @@ -3,13 +3,12 @@ package me.trouper.alias.server.systems; import me.trouper.alias.server.Main; import org.bukkit.Bukkit; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; +import java.io.Closeable; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; -public class TaskManager implements Main { - private final ConcurrentHashMap tasks = new ConcurrentHashMap<>(); +public class TaskManager implements Closeable, Main { + private final Map tasks = new HashMap<>(); private volatile boolean closed = false; public int scheduleTask(Runnable task, long delay) { @@ -22,7 +21,7 @@ public class TaskManager implements Main { }, delay).getTaskId(); if (!closed) { - tasks.put(taskId, Boolean.TRUE); + tasks.put(taskId,Boolean.TRUE); return taskId; } else { Bukkit.getScheduler().cancelTask(taskId); @@ -30,6 +29,7 @@ public class TaskManager implements Main { } } + @Override public void close() { closed = true; tasks.keySet().forEach(Bukkit.getScheduler()::cancelTask); diff --git a/src/main/java/me/trouper/alias/server/systems/Verbose.java b/src/main/java/me/trouper/alias/server/systems/Verbose.java index 82a177b..4e4e3ce 100644 --- a/src/main/java/me/trouper/alias/server/systems/Verbose.java +++ b/src/main/java/me/trouper/alias/server/systems/Verbose.java @@ -3,7 +3,9 @@ package me.trouper.alias.server.systems; import me.trouper.alias.server.Main; import net.kyori.adventure.text.Component; import org.bukkit.Bukkit; +import org.bukkit.Location; import org.bukkit.entity.Player; +import org.bukkit.util.Vector; public class Verbose implements Main { @@ -26,12 +28,18 @@ public class Verbose implements Main { if (className.contains("-")) callerInfo = "Protected"; else callerInfo = className + "." + caller.getMethodName(); - if (main.getCommon().getDebuggerExclusions().contains(callerInfo)) return; } - Component message = Text.format(Text.Pallet.INFO,verbose,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); + Object[] processedArgs = processArgs(args); + 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); @@ -49,4 +57,24 @@ public class Verbose implements Main { public static void send(String verbose, Object... 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; + } } diff --git a/src/main/java/me/trouper/alias/server/systems/burning/BlockBurner.java b/src/main/java/me/trouper/alias/server/systems/burning/BlockBurner.java index 5b5a79d..ba28de7 100644 --- a/src/main/java/me/trouper/alias/server/systems/burning/BlockBurner.java +++ b/src/main/java/me/trouper/alias/server/systems/burning/BlockBurner.java @@ -2,7 +2,6 @@ package me.trouper.alias.server.systems.burning; import me.trouper.alias.server.Main; import me.trouper.alias.server.systems.TaskManager; -import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; @@ -27,12 +26,6 @@ public class BlockBurner implements Closeable, Main { this.taskManager = new TaskManager(); } - public BlockBurner(BurnOptions options, TaskManager sharedTaskManager) { - this.options = options; - this.palette = new BurnPalette(); - this.taskManager = sharedTaskManager; - } - @Override public void close() { taskManager.close(); @@ -40,8 +33,8 @@ public class BlockBurner implements Closeable, Main { burning.clear(); } - public void burn(Block block, float heat) { - if (isOccluded(block)) return; + public void burn(Block block, float heat, boolean checkOcclusion) { + if (checkOcclusion && isOccluded(block)) return; if (visited.contains(block)) return; visited.add(block); @@ -77,7 +70,7 @@ public class BlockBurner implements Closeable, Main { long totalDelay = 0; for (BurnStage stage : stages) { - totalDelay += stage.getDelay(); + totalDelay += stage.getDelayTicks(); taskManager.scheduleTask(() -> { if (block.getType().isAir()) return; diff --git a/src/main/java/me/trouper/alias/server/systems/burning/BurnOptions.java b/src/main/java/me/trouper/alias/server/systems/burning/BurnOptions.java index f5f6812..8291729 100644 --- a/src/main/java/me/trouper/alias/server/systems/burning/BurnOptions.java +++ b/src/main/java/me/trouper/alias/server/systems/burning/BurnOptions.java @@ -2,7 +2,7 @@ package me.trouper.alias.server.systems.burning; public class BurnOptions { private boolean disabled = false; - private double setFireChance = 1.0 / 80; + private double setFireChance = 0.2; public boolean isDisabled() { return disabled; } public void setDisabled(boolean disabled) { this.disabled = disabled; } diff --git a/src/main/java/me/trouper/alias/server/systems/burning/BurnStage.java b/src/main/java/me/trouper/alias/server/systems/burning/BurnStage.java index 1e6d757..32fbeba 100644 --- a/src/main/java/me/trouper/alias/server/systems/burning/BurnStage.java +++ b/src/main/java/me/trouper/alias/server/systems/burning/BurnStage.java @@ -3,14 +3,14 @@ package me.trouper.alias.server.systems.burning; import org.bukkit.block.data.BlockData; public class BurnStage { - private final long delay; + private final long delayTicks; private final BlockData blockData; - public BurnStage(long delay, BlockData blockData) { - this.delay = delay; + public BurnStage(long delayTicks, BlockData blockData) { + this.delayTicks = delayTicks; this.blockData = blockData; } - public long getDelay() { return delay; } + public long getDelayTicks() { return delayTicks; } public BlockData getBlockData() { return blockData; } } diff --git a/src/main/java/me/trouper/alias/server/systems/tracing/CustomDisplayRaytracer.java b/src/main/java/me/trouper/alias/server/systems/tracing/CustomDisplayRaytracer.java index c870446..78c8739 100755 --- a/src/main/java/me/trouper/alias/server/systems/tracing/CustomDisplayRaytracer.java +++ b/src/main/java/me/trouper/alias/server/systems/tracing/CustomDisplayRaytracer.java @@ -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(); } - public static Point trace(Location start, Location end, Predicate hitCondition) { return trace(start, end, 0.5, hitCondition); } public static Point trace(Location start, Location end, double interval, Predicate 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 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 hitCondition) { @@ -285,7 +287,8 @@ public class CustomDisplayRaytracer { } 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) { diff --git a/src/main/java/me/trouper/alias/server/systems/visual/DisplayUtils.java b/src/main/java/me/trouper/alias/server/systems/visual/DisplayUtils.java index 3522fa8..faa4d06 100755 --- a/src/main/java/me/trouper/alias/server/systems/visual/DisplayUtils.java +++ b/src/main/java/me/trouper/alias/server/systems/visual/DisplayUtils.java @@ -27,11 +27,6 @@ public class DisplayUtils implements Main { return l -> l.getWorld().spawnParticle(Particle.DUST, l, 1, 0, 0, 0, 0, dust); }; - public static final Function> 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) { ring(loc, radius, DUST_PARTICLE_FACTORY.apply(color, thickness)); } diff --git a/src/main/java/me/trouper/alias/server/systems/visual/ParticleUtils.java b/src/main/java/me/trouper/alias/server/systems/visual/ParticleUtils.java new file mode 100644 index 0000000..2236419 --- /dev/null +++ b/src/main/java/me/trouper/alias/server/systems/visual/ParticleUtils.java @@ -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 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 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); + } + } + } + } + } +} diff --git a/src/main/java/me/trouper/alias/server/systems/world/BlockHistory.java b/src/main/java/me/trouper/alias/server/systems/world/BlockHistory.java new file mode 100644 index 0000000..d4a0ae8 --- /dev/null +++ b/src/main/java/me/trouper/alias/server/systems/world/BlockHistory.java @@ -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 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 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 getCurrentBlocks() { + lock.lock(); + try { + if (currentIndex >= 0 && currentIndex < history.size()) { + return history.get(currentIndex).getBlocks(); + } + return Collections.emptySet(); + } finally { + lock.unlock(); + } + } +} diff --git a/src/main/java/me/trouper/alias/server/systems/world/ExplosionOptions.java b/src/main/java/me/trouper/alias/server/systems/world/ExplosionOptions.java new file mode 100644 index 0000000..bc763a5 --- /dev/null +++ b/src/main/java/me/trouper/alias/server/systems/world/ExplosionOptions.java @@ -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; } +} diff --git a/src/main/java/me/trouper/alias/server/systems/world/ExplosionResult.java b/src/main/java/me/trouper/alias/server/systems/world/ExplosionResult.java new file mode 100644 index 0000000..a30a007 --- /dev/null +++ b/src/main/java/me/trouper/alias/server/systems/world/ExplosionResult.java @@ -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 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; + } +} \ No newline at end of file diff --git a/src/main/java/me/trouper/alias/server/systems/world/ExplosionUtils.java b/src/main/java/me/trouper/alias/server/systems/world/ExplosionUtils.java index 48da469..2da0392 100644 --- a/src/main/java/me/trouper/alias/server/systems/world/ExplosionUtils.java +++ b/src/main/java/me/trouper/alias/server/systems/world/ExplosionUtils.java @@ -1,186 +1,78 @@ package me.trouper.alias.server.systems.world; 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.burning.BlockBurner; -import me.trouper.alias.server.systems.burning.BurnOptions; -import me.trouper.alias.utils.TargetingUtils; +import me.trouper.alias.utils.SoundPlayer; import org.bukkit.*; import org.bukkit.block.Block; -import org.bukkit.block.BlockState; 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 java.io.Closeable; import java.util.*; import java.util.concurrent.ThreadLocalRandom; 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 originalStates = new HashMap<>(); - private final Map 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 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 getOriginalStates() { - return originalStates; - } - - public Map getOriginalInventories() { - return originalInventories; - } - } - public static ExplosionResult createExplosion(Location center, ExplosionOptions options) { World world = center.getWorld(); if (world == null) throw new IllegalArgumentException("Center location must have a valid world"); double maxBurnRadius = options.getMaxBurnRadius(); - Map affectedBlocks = getBlocksInRadius(center, maxBurnRadius); - Map affectedEntities = getEntitiesInRadius(center, maxBurnRadius); + try { + Map affectedBlocks = getBlocksInRadius(center, maxBurnRadius); + Map affectedEntities = getEntitiesInRadius(center, maxBurnRadius); - TaskManager sharedTaskManager = new TaskManager(); - BlockBurner burner = new BlockBurner(options.getBurnOptions(), sharedTaskManager); - ExplosionResult result = new ExplosionResult(burner); + BlockBurner burner = new BlockBurner(options.getBurnOptions()); + TaskManager sharedTaskManager = burner.getTaskManager(); + ExplosionResult result = new ExplosionResult(affectedBlocks.keySet(), sharedTaskManager); - for (Block block : affectedBlocks.keySet()) { - if (block.getType().isAir()) continue; - result.recordSnapshot(block); + List blocksToDestroy = new ArrayList<>(); + List blocksToBurn = new ArrayList<>(); + Map blocksHeatMap = new HashMap<>(affectedBlocks.size()); + + categorizeBlocks(affectedBlocks, options, blocksToDestroy, blocksToBurn, blocksHeatMap); + + scheduleDestruction(blocksToDestroy, options, sharedTaskManager); + scheduleBurning(blocksToBurn, blocksHeatMap, burner, center, options, sharedTaskManager); + scheduleDamage(affectedEntities, options, sharedTaskManager); + + if (options.isCreateParticles() || options.isPlaySound()) { + createExplosionEffects(center, options); + } + + return result; + + } catch (Exception e) { + System.err.println("Failed to create explosion: " + e.getMessage()); + throw e; } - - Set blocksToDestroy = new HashSet<>(); - Set blocksToBurn = new HashSet<>(); - Map blocksHeatMap = new HashMap<>(); - - categorizeBlocks(affectedBlocks, options, blocksToDestroy, blocksToBurn, blocksHeatMap); - - scheduleDestruction(blocksToDestroy, options, sharedTaskManager); - scheduleBurning(blocksToBurn, blocksHeatMap, burner, center, options, sharedTaskManager); - scheduleDamage(affectedEntities, options, sharedTaskManager); - - if (options.isCreateParticles() || options.isPlaySound()) createExplosionEffects(center, options); - - return result; } private static Map getBlocksInRadius(Location center, double radius) { - Map blocks = new HashMap<>(); World world = center.getWorld(); + if (world == null) return Collections.emptyMap(); int radiusInt = (int) Math.ceil(radius); - Vector centerVec = center.toVector(); + + int estimatedBlocks = (int) ((4.0 / 3.0) * Math.PI * Math.pow(radius, 3)); + Map 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 y = -radiusInt; y <= radiusInt; y++) { for (int z = -radiusInt; z <= radiusInt; z++) { - Block block = world.getBlockAt( - center.getBlockX() + x, - center.getBlockY() + y, - center.getBlockZ() + z - ); + double distanceSquared = x*x + y*y + z*z; + if (distanceSquared > radiusSquared) continue; - double distance = block.getLocation().toVector().distance(centerVec); - if (distance <= radius) { + Block block = world.getBlockAt(centerX + x, centerY + y, centerZ + z); + if (block != null && !block.getType().isAir()) { + double distance = Math.sqrt(distanceSquared); blocks.put(block, distance); } } @@ -190,22 +82,33 @@ public class ExplosionUtils implements Main { return blocks; } - public static Map getEntitiesInRadius(Location center, double radius) { - List rawList = center.getNearbyLivingEntities(radius).stream().toList(); - Map entities = new HashMap<>(); + private static Map getEntitiesInRadius(Location center, double radius) { + List rawList = center.getNearbyEntities(radius, radius, radius).stream() + .filter(entity -> entity instanceof LivingEntity) + .map(entity -> (LivingEntity) entity) + .toList(); + + if (rawList.isEmpty()) return Collections.emptyMap(); + + Map entities = new HashMap<>(rawList.size() * 4 / 3 + 1); 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; } private static void categorizeBlocks(Map affectedBlocks, ExplosionOptions options, - Set blocksToDestroy, Set blocksToBurn, + List blocksToDestroy, List blocksToBurn, Map blocksHeatMap) { ThreadLocalRandom random = ThreadLocalRandom.current(); + int totalBlocks = affectedBlocks.size(); + List tempBlocksToBurn = new ArrayList<>(totalBlocks / 2); + for (Map.Entry entry : affectedBlocks.entrySet()) { Block block = entry.getKey(); double distance = entry.getValue(); @@ -215,7 +118,11 @@ public class ExplosionUtils implements Main { float heat = calculateHeat(distance, options); 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()) { double destructionChance = 1.0 - ((distance - options.getCoreRadius()) / @@ -223,14 +130,26 @@ public class ExplosionUtils implements Main { destructionChance *= (0.7 + random.nextDouble() * 0.6); - if (random.nextDouble() < destructionChance) blocksToDestroy.add(block); - else blocksToBurn.add(block); + if (random.nextDouble() < destructionChance) { + blocksToDestroy.add(block); + } else { + tempBlocksToBurn.add(block); + } } else { double burnChance = 1.0 - ((distance - options.getFalloffRadius()) / (options.getMaxBurnRadius() - options.getFalloffRadius())); burnChance *= (0.8 + random.nextDouble() * 0.6); - if (random.nextDouble() < burnChance) blocksToBurn.add(block); + if (random.nextDouble() < burnChance) { + tempBlocksToBurn.add(block); + } + } + } + + Set 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) { double normalizedDistance = distance / options.getMaxBurnRadius(); double heatRange = options.getMaxHeat() - options.getMinHeat(); - double heatFactor = Math.pow(1.0 - normalizedDistance, 2.0); - return (float) (options.getMinHeat() + heatRange * heatFactor); } - private static void scheduleDestruction(Set blocksToDestroy, ExplosionOptions options, TaskManager taskManager) { + private static void scheduleDestruction(List blocksToDestroy, ExplosionOptions options, TaskManager taskManager) { if (blocksToDestroy.isEmpty()) return; long destructionDelayTicks = (long) (options.getDestructionDelay() * 20); taskManager.scheduleTask(() -> { - List blockList = new ArrayList<>(blocksToDestroy); - Collections.shuffle(blockList); - - int blocksPerWave = Math.max(1, blockList.size() / 5); + Collections.shuffle(blocksToDestroy); + int blocksPerWave = Math.max(1, blocksToDestroy.size() / 5); for (int wave = 0; wave < 5; wave++) { 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(() -> { - for (int i = startIndex; i < endIndex; i++) { - Block block = blockList.get(i); - if (!block.getType().isAir()) { - block.setType(Material.AIR); + for (int i = finalStartIndex; i < finalEndIndex; i++) { + if (i < blocksToDestroy.size()) { + Block block = blocksToDestroy.get(i); + if (block != null && !block.getType().isAir()) { + block.setType(Material.AIR); + } } } - }, wave * 2); + }, wave * 2L); } }, destructionDelayTicks); } - private static void scheduleBurning(Set blocksToMaybeBurn, Map blocksHeatMap, + private static void scheduleBurning(List blocksToMaybeBurn, Map blocksHeatMap, BlockBurner burner, Location center, ExplosionOptions options, TaskManager taskManager) { if (blocksToMaybeBurn.isEmpty()) return; @@ -281,16 +201,16 @@ public class ExplosionUtils implements Main { long burnDelayTicks = (long) (options.getBurnDelay() * 20); taskManager.scheduleTask(() -> { - List blockList = new ArrayList<>(blocksToMaybeBurn); - Collections.shuffle(blockList); + Collections.shuffle(blocksToMaybeBurn); Map> burnWaves = new HashMap<>(); - for (Block block : blockList) { - double distance = block.getLocation().distance(center); - int waveIndex = (int) (distance / 2.0); - - burnWaves.computeIfAbsent(waveIndex, k -> new ArrayList<>()).add(block); + for (Block block : blocksToMaybeBurn) { + if (block != null) { + double distance = block.getLocation().distance(center); + int waveIndex = (int) (distance / 2.0); + burnWaves.computeIfAbsent(waveIndex, k -> new ArrayList<>()).add(block); + } } for (Map.Entry> waveEntry : burnWaves.entrySet()) { @@ -298,17 +218,17 @@ public class ExplosionUtils implements Main { List waveBlocks = waveEntry.getValue(); taskManager.scheduleTask(() -> { + ThreadLocalRandom random = ThreadLocalRandom.current(); + for (Block block : waveBlocks) { - if (burner.isClosed()) continue; + if (burner.isClosed() || block == null) continue; float heat = blocksHeatMap.getOrDefault(block, 0.1f); - - ThreadLocalRandom random = ThreadLocalRandom.current(); int randomDelay = random.nextInt(0, 10); taskManager.scheduleTask(() -> { - if (!burner.isClosed() && !block.getType().isAir()) { - burner.burn(block, heat); + if (!burner.isClosed() && block != null && !block.getType().isAir()) { + burner.burn(block, heat, false); } }, randomDelay); } @@ -318,36 +238,43 @@ public class ExplosionUtils implements Main { } private static void scheduleDamage(Map affected, ExplosionOptions options, TaskManager taskManager) { + if (affected.isEmpty()) return; + double baseDamage = options.getBaseDamage(); double igniteDistance = options.getMaxBurnRadius(); double halfDamageDistance = options.getFalloffRadius(); double fullDamageDistance = options.getCoreRadius(); for (Map.Entry entityDistance : affected.entrySet()) { - LivingEntity liv = (LivingEntity) Bukkit.getEntity(entityDistance.getKey()); - if (liv == null) continue; + UUID entityId = entityDistance.getKey(); + if (entityId == null) continue; double distance = entityDistance.getValue(); + long delay = Math.max(1, (long) distance / 2); if (distance >= halfDamageDistance && distance <= igniteDistance) { - taskManager.scheduleTask(()->{ - liv.setFireTicks(5 * 20); - },(long) distance / 2); - return; - } - if (distance >= fullDamageDistance && distance <= halfDamageDistance) { - taskManager.scheduleTask(()->{ - liv.setFireTicks(10 * 20); - liv.damage(baseDamage / 2); - },(long) distance / 2); - return; - } - if (distance <= fullDamageDistance) { - taskManager.scheduleTask(()->{ - liv.setFireTicks(15 * 20); - liv.damage(baseDamage); - },(long) distance / 2); - return; + taskManager.scheduleTask(() -> { + LivingEntity entity = (LivingEntity) Bukkit.getEntity(entityId); + if (entity != null && entity.isValid()) { + entity.setFireTicks(5 * 20); + } + }, delay); + } else if (distance >= fullDamageDistance && distance <= halfDamageDistance) { + taskManager.scheduleTask(() -> { + LivingEntity entity = (LivingEntity) Bukkit.getEntity(entityId); + if (entity != null && entity.isValid()) { + entity.setFireTicks(10 * 20); + entity.damage(baseDamage / 2); + } + }, delay); + } else if (distance <= fullDamageDistance) { + taskManager.scheduleTask(() -> { + LivingEntity entity = (LivingEntity) Bukkit.getEntity(entityId); + if (entity != null && entity.isValid()) { + entity.setFireTicks(15 * 20); + entity.damage(baseDamage); + } + }, delay); } } } @@ -356,9 +283,12 @@ public class ExplosionUtils implements Main { World world = center.getWorld(); if (world == null) return; + double soundRange = options.getMaxBurnRadius() * 20; + if (options.isPlaySound()) { - world.playSound(center, Sound.ENTITY_GENERIC_EXPLODE, 1.0f, 0.8f); - world.playSound(center, Sound.ENTITY_LIGHTNING_BOLT_THUNDER, 0.5f, 1.2f); + new SoundPlayer(Sound.ENTITY_WARDEN_SONIC_BOOM, 40, 0.5F).playAt(center, soundRange); + 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()) { @@ -368,22 +298,25 @@ public class ExplosionUtils implements Main { world.spawnParticle(Particle.FLAME, center, 30, 3, 3, 3, 0.1); Bukkit.getScheduler().runTaskLater(main.getPlugin(), () -> { - world.spawnParticle(Particle.SMOKE, center, 50, 4, 4, 4, 0.02); - }, 20); + if (center.getWorld() != null) { + center.getWorld().spawnParticle(Particle.SMOKE, center, 50, 4, 4, 4, 0.02); + } + }, 20L); } } - public static ExplosionResult createExplosion(Location center) { - return createExplosion(center, new ExplosionOptions()); + private static boolean isOccluded(Block block, Set doesNotOcclude) { + 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) { - ExplosionOptions options = new ExplosionOptions(); - options.setCoreRadius(coreRadius); - options.setFalloffRadius(falloffRadius); - options.setMaxBurnRadius(maxBurnRadius); - options.setBurnDelay(0); - options.setDestructionDelay(0); - return createExplosion(center, options); + private static boolean isOccluding(Block block, Set doesNotOcclude) { + return block != null && block.getType().isOccluding() && !doesNotOcclude.contains(block); } } \ No newline at end of file diff --git a/src/main/java/me/trouper/alias/server/systems/world/Snapshot.java b/src/main/java/me/trouper/alias/server/systems/world/Snapshot.java index f97e287..8d9d064 100644 --- a/src/main/java/me/trouper/alias/server/systems/world/Snapshot.java +++ b/src/main/java/me/trouper/alias/server/systems/world/Snapshot.java @@ -1,5 +1,85 @@ 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 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 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 getInventory() { + return inventory; + } +} \ No newline at end of file diff --git a/src/main/java/me/trouper/alias/server/systems/world/State.java b/src/main/java/me/trouper/alias/server/systems/world/State.java new file mode 100644 index 0000000..542cc19 --- /dev/null +++ b/src/main/java/me/trouper/alias/server/systems/world/State.java @@ -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 snapshots; + private volatile boolean closed = false; + + public State(Collection 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 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 getBlocks() { + if (closed || snapshots == null) { + return new HashSet<>(); + } + + Set 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; + } +} \ No newline at end of file diff --git a/src/main/java/me/trouper/alias/utils/FormatUtils.java b/src/main/java/me/trouper/alias/utils/FormatUtils.java new file mode 100644 index 0000000..70ff6f3 --- /dev/null +++ b/src/main/java/me/trouper/alias/utils/FormatUtils.java @@ -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(); + } +} diff --git a/src/main/java/me/trouper/alias/utils/InventoryUtils.java b/src/main/java/me/trouper/alias/utils/InventoryUtils.java new file mode 100644 index 0000000..82d9696 --- /dev/null +++ b/src/main/java/me/trouper/alias/utils/InventoryUtils.java @@ -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; + } +} +