diff --git a/eternalminions-plugin/src/main/java/com/eternalcode/minions/EternalMinionsPlugin.java b/eternalminions-plugin/src/main/java/com/eternalcode/minions/EternalMinionsPlugin.java index 9b48172..05c586e 100644 --- a/eternalminions-plugin/src/main/java/com/eternalcode/minions/EternalMinionsPlugin.java +++ b/eternalminions-plugin/src/main/java/com/eternalcode/minions/EternalMinionsPlugin.java @@ -31,6 +31,7 @@ import com.eternalcode.minions.minion.MinionManagement; import com.eternalcode.minions.minion.MinionPickupService; import com.eternalcode.minions.minion.controller.MinionBlockProtectionController; +import com.eternalcode.minions.minion.controller.MinionChestLinkController; import com.eternalcode.minions.minion.controller.MinionPistonProtectionController; import com.eternalcode.minions.minion.controller.MinionPlacementController; import com.eternalcode.minions.minion.MinionServiceImpl; @@ -286,6 +287,7 @@ public void onEnable() { new MinionViewerListener(renders), new MinionPistonProtectionController(this.minions), new MinionBlockProtectionController(this.minions, messages, notices), + new MinionChestLinkController(this.minions, lifecycle, messages, notices), chestLinks, new MinionPlacementController( minionItems, diff --git a/eternalminions-plugin/src/main/java/com/eternalcode/minions/config/MessagesConfig.java b/eternalminions-plugin/src/main/java/com/eternalcode/minions/config/MessagesConfig.java index e5e3009..26eb5a8 100644 --- a/eternalminions-plugin/src/main/java/com/eternalcode/minions/config/MessagesConfig.java +++ b/eternalminions-plugin/src/main/java/com/eternalcode/minions/config/MessagesConfig.java @@ -45,6 +45,8 @@ public Path resolve(Path dataDirectory) { public Notice chestUnlinked = Notice.chat(prefix() + "Chest unlinked from the minion."); public Notice chestLinkTooFar = Notice.chat(prefix() + "That chest is too far away from the minion."); public Notice chestLinkExpired = Notice.chat(prefix() + "Chest selection timed out."); + public Notice chestLinkDestroyed = + Notice.chat(prefix() + "The container linked to your minion was destroyed, so the link was removed."); public Notice minionRotated = Notice.chat(prefix() + "Minion rotated."); diff --git a/eternalminions-plugin/src/main/java/com/eternalcode/minions/minion/MinionRegistry.java b/eternalminions-plugin/src/main/java/com/eternalcode/minions/minion/MinionRegistry.java index a76e837..07946e4 100644 --- a/eternalminions-plugin/src/main/java/com/eternalcode/minions/minion/MinionRegistry.java +++ b/eternalminions-plugin/src/main/java/com/eternalcode/minions/minion/MinionRegistry.java @@ -3,10 +3,13 @@ import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import it.unimi.dsi.fastutil.longs.LongOpenHashSet; import it.unimi.dsi.fastutil.longs.LongIterator; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.UUID; import java.util.function.LongConsumer; @@ -16,6 +19,7 @@ public final class MinionRegistry { private final Long2ObjectOpenHashMap minions = new Long2ObjectOpenHashMap<>(); private final Collection minionView = Collections.unmodifiableCollection(this.minions.values()); private final Map> minionsByWorldChunk = new HashMap<>(); + private final Map> chestLinksByWorldChunk = new HashMap<>(); private long lastIssuedId = System.currentTimeMillis(); public MinionId createId() { @@ -33,6 +37,7 @@ public void register(Minion minion) { } this.lastIssuedId = Math.max(this.lastIssuedId, id); this.index(minion); + this.indexChestLink(minion); } public Minion replace(Minion minion) { @@ -45,6 +50,10 @@ public Minion replace(Minion minion) { this.unindex(previous); this.index(minion); } + if (!Objects.equals(previous.chestPosition(), minion.chestPosition())) { + this.unindexChestLink(previous); + this.indexChestLink(minion); + } return previous; } @@ -52,6 +61,7 @@ public Optional remove(MinionId minionId) { Minion removed = this.minions.remove(minionId.value()); if (removed != null) { this.unindex(removed); + this.unindexChestLink(removed); } return Optional.ofNullable(removed); } @@ -140,6 +150,78 @@ public int countByOwner(UUID ownerId) { return count; } + public List findLinkedToChest(String worldKey, int blockX, int blockY, int blockZ) { + Long2ObjectOpenHashMap worldChunks = this.chestLinksByWorldChunk.get(worldKey); + + if (worldChunks == null) { + return List.of(); + } + + LongOpenHashSet ids = worldChunks.get(chunkKey(blockX >> 4, blockZ >> 4)); + + if (ids == null) { + return List.of(); + } + + List linked = new ArrayList<>(); + LongIterator iterator = ids.iterator(); + + while (iterator.hasNext()) { + Minion minion = this.minions.get(iterator.nextLong()); + + if (minion == null) { + continue; + } + + MinionPosition chestPosition = minion.chestPosition(); + + if (chestPosition != null + && chestPosition.blockX() == blockX + && chestPosition.blockY() == blockY + && chestPosition.blockZ() == blockZ) { + linked.add(minion); + } + } + + return linked; + } + + private void indexChestLink(Minion minion) { + MinionPosition chestPosition = minion.chestPosition(); + if (chestPosition == null) { + return; + } + long chunkKey = chunkKey(chestPosition.blockX() >> 4, chestPosition.blockZ() >> 4); + this.chestLinksByWorldChunk + .computeIfAbsent(chestPosition.worldKey(), ignored -> new Long2ObjectOpenHashMap<>()) + .computeIfAbsent(chunkKey, ignored -> new LongOpenHashSet()) + .add(minion.id().value()); + } + + private void unindexChestLink(Minion minion) { + MinionPosition chestPosition = minion.chestPosition(); + if (chestPosition == null) { + return; + } + Long2ObjectOpenHashMap worldChunks = + this.chestLinksByWorldChunk.get(chestPosition.worldKey()); + if (worldChunks == null) { + return; + } + long chunkKey = chunkKey(chestPosition.blockX() >> 4, chestPosition.blockZ() >> 4); + LongOpenHashSet ids = worldChunks.get(chunkKey); + if (ids == null) { + return; + } + ids.remove(minion.id().value()); + if (ids.isEmpty()) { + worldChunks.remove(chunkKey); + } + if (worldChunks.isEmpty()) { + this.chestLinksByWorldChunk.remove(chestPosition.worldKey()); + } + } + private void index(Minion minion) { String worldKey = minion.position().worldKey(); long chunkKey = chunkKey(minion.position().blockX() >> 4, minion.position().blockZ() >> 4); diff --git a/eternalminions-plugin/src/main/java/com/eternalcode/minions/minion/controller/MinionChestLinkController.java b/eternalminions-plugin/src/main/java/com/eternalcode/minions/minion/controller/MinionChestLinkController.java new file mode 100644 index 0000000..047ca32 --- /dev/null +++ b/eternalminions-plugin/src/main/java/com/eternalcode/minions/minion/controller/MinionChestLinkController.java @@ -0,0 +1,136 @@ +package com.eternalcode.minions.minion.controller; + +import com.eternalcode.minions.config.MessagesConfig; +import com.eternalcode.minions.minion.Minion; +import com.eternalcode.minions.minion.MinionLifecycleService; +import com.eternalcode.minions.minion.MinionPosition; +import com.eternalcode.minions.minion.MinionRegistry; +import com.eternalcode.minions.notice.NoticeService; +import java.util.List; +import org.bukkit.Bukkit; +import org.bukkit.block.Block; +import org.bukkit.block.Chest; +import org.bukkit.block.Container; +import org.bukkit.entity.Player; +import org.bukkit.inventory.DoubleChestInventory; +import org.bukkit.inventory.InventoryHolder; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockBurnEvent; +import org.bukkit.event.block.BlockExplodeEvent; +import org.bukkit.event.entity.EntityExplodeEvent; + +public final class MinionChestLinkController implements Listener { + + private final MinionRegistry minions; + private final MinionLifecycleService lifecycle; + private final MessagesConfig messages; + private final NoticeService notices; + + public MinionChestLinkController( + MinionRegistry minions, + MinionLifecycleService lifecycle, + MessagesConfig messages, + NoticeService notices + ) { + this.minions = minions; + this.lifecycle = lifecycle; + this.messages = messages; + this.notices = notices; + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlockBreak(BlockBreakEvent event) { + this.unlink(event.getBlock(), List.of()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlockBurn(BlockBurnEvent event) { + this.unlink(event.getBlock(), List.of()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlockExplode(BlockExplodeEvent event) { + this.unlinkAll(event.blockList()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onEntityExplode(EntityExplodeEvent event) { + this.unlinkAll(event.blockList()); + } + + private void unlinkAll(List destroyed) { + for (Block block : List.copyOf(destroyed)) { + this.unlink(block, destroyed); + } + } + + private void unlink(Block block, List destroyed) { + if (!(block.getState(false) instanceof Container)) { + return; + } + + List linked = this.minions.findLinkedToChest( + block.getWorld().getKey().asString(), + block.getX(), + block.getY(), + block.getZ() + ); + + if (linked.isEmpty()) { + return; + } + + MinionPosition remainingHalf = this.remainingHalf(block, destroyed); + + for (Minion minion : linked) { + if (remainingHalf != null) { + this.lifecycle.updateChestLink(minion.withChestPosition(remainingHalf)); + continue; + } + this.lifecycle.updateChestLink(minion.withChestPosition(null)); + this.notifyOwner(minion); + } + } + + private MinionPosition remainingHalf(Block block, List destroyed) { + if (!(block.getState(false) instanceof Chest chest) + || !(chest.getInventory() instanceof DoubleChestInventory doubleChest)) { + return null; + } + + Block left = holderBlock(doubleChest.getLeftSide().getHolder()); + Block right = holderBlock(doubleChest.getRightSide().getHolder()); + Block other = block.equals(left) ? right : block.equals(right) ? left : null; + + if (other == null || destroyed.contains(other)) { + return null; + } + + return new MinionPosition( + other.getWorld().getKey().asString(), + other.getX(), + other.getY(), + other.getZ() + ); + } + + private static Block holderBlock(InventoryHolder holder) { + return holder instanceof Chest chest ? chest.getBlock() : null; + } + + private void notifyOwner(Minion minion) { + Player owner = Bukkit.getPlayer(minion.ownerId()); + + if (owner == null) { + return; + } + + this.notices.create() + .viewer(owner) + .notice(this.messages.chestLinkDestroyed) + .send(); + } +}