diff --git a/.idea/libraries/SpigotWizCompat.xml b/.idea/libraries/SpigotWizCompat.xml
deleted file mode 100644
index 6418489..0000000
--- a/.idea/libraries/SpigotWizCompat.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/dev/w1zzrd/invtweaks/InvTweaksPlugin.java b/src/dev/w1zzrd/invtweaks/InvTweaksPlugin.java
index c570dea..3e6363e 100644
--- a/src/dev/w1zzrd/invtweaks/InvTweaksPlugin.java
+++ b/src/dev/w1zzrd/invtweaks/InvTweaksPlugin.java
@@ -2,7 +2,9 @@ package dev.w1zzrd.invtweaks;
import dev.w1zzrd.invtweaks.command.*;
import dev.w1zzrd.invtweaks.enchantment.CapitatorEnchantment;
+import dev.w1zzrd.invtweaks.feature.NamedChestManager;
import dev.w1zzrd.invtweaks.listener.*;
+import dev.w1zzrd.invtweaks.serialization.ChestNameConfig;
import dev.w1zzrd.invtweaks.serialization.MagnetConfig;
import dev.w1zzrd.invtweaks.serialization.MagnetData;
import dev.w1zzrd.invtweaks.serialization.SearchConfig;
@@ -41,6 +43,7 @@ public final class InvTweaksPlugin extends JavaPlugin {
private NamedChestCommand namedChestCommandExecutor;
private CapitatorCommand capitatorCommand;
private PersistentData data;
+ private NamedChestManager chestManager;
private EnchantmentRegistryEntry capitatorEnchantment = null;
@Override
@@ -118,6 +121,8 @@ public final class InvTweaksPlugin extends JavaPlugin {
pluginManager.registerEvents(new MagnetismListener(magnetCommandExecutor), this);
pluginManager.registerEvents(new TabCompletionListener(), this);
pluginManager.registerEvents(new TreeCapitatorListener(activateCapitator ? capitatorEnchantment.getEnchantment() : null), this);
+ pluginManager.registerEvents(new PlayerMoveRenderListener(chestManager), this);
+ pluginManager.registerEvents(new ChestBreakListener(chestManager), this);
}
/**
@@ -137,7 +142,7 @@ public final class InvTweaksPlugin extends JavaPlugin {
sortCommandExecutor = new SortCommandExecutor();
magnetCommandExecutor = new MagnetCommandExecutor(this, "magnet", getPersistentData());
searchCommandExecutor = new SearchCommandExecutor(this, "search");
- namedChestCommandExecutor = new NamedChestCommand(this);
+ namedChestCommandExecutor = new NamedChestCommand(chestManager);
if (activateCapitator)
capitatorCommand = new CapitatorCommand(capitatorEnchantment.getEnchantment());
@@ -174,6 +179,10 @@ public final class InvTweaksPlugin extends JavaPlugin {
ConfigurationSerialization.registerClass(MagnetConfig.class);
ConfigurationSerialization.registerClass(MagnetData.class);
ConfigurationSerialization.registerClass(SearchConfig.class);
+ ConfigurationSerialization.registerClass(ChestNameConfig.class);
+ ConfigurationSerialization.registerClass(ChestNameConfig.ChestNameWorldEntry.class);
+ ConfigurationSerialization.registerClass(ChestNameConfig.ChestNameWorldEntry.ChestNameChunkEntry.class);
+ ConfigurationSerialization.registerClass(ChestNameConfig.ChestNameWorldEntry.ChestNameChunkEntry.ChestNameConfigEntry.class);
}
/**
@@ -182,6 +191,10 @@ public final class InvTweaksPlugin extends JavaPlugin {
* @see #registerSerializers()
*/
private void unregisterSerializers() {
+ ConfigurationSerialization.registerClass(ChestNameConfig.ChestNameWorldEntry.ChestNameChunkEntry.ChestNameConfigEntry.class);
+ ConfigurationSerialization.registerClass(ChestNameConfig.ChestNameWorldEntry.ChestNameChunkEntry.class);
+ ConfigurationSerialization.registerClass(ChestNameConfig.ChestNameWorldEntry.class);
+ ConfigurationSerialization.registerClass(ChestNameConfig.class);
ConfigurationSerialization.unregisterClass(MagnetConfig.class);
ConfigurationSerialization.unregisterClass(MagnetData.class);
ConfigurationSerialization.unregisterClass(SearchConfig.class);
@@ -199,12 +212,16 @@ public final class InvTweaksPlugin extends JavaPlugin {
// Implicit load
data = new PersistentData(PERSISTENT_DATA_NAME, this);
+
+ chestManager = new NamedChestManager(data);
}
/**
* De-activate and finalize persistent data storage sources and handlers
*/
private void disablePersistentData() {
+ chestManager = null;
+
data.saveData();
data = null;
diff --git a/src/dev/w1zzrd/invtweaks/command/NamedChestCommand.java b/src/dev/w1zzrd/invtweaks/command/NamedChestCommand.java
index 0739a13..10ef873 100644
--- a/src/dev/w1zzrd/invtweaks/command/NamedChestCommand.java
+++ b/src/dev/w1zzrd/invtweaks/command/NamedChestCommand.java
@@ -1,29 +1,23 @@
package dev.w1zzrd.invtweaks.command;
-import org.bukkit.Bukkit;
-import org.bukkit.Location;
+import dev.w1zzrd.invtweaks.feature.NamedChestManager;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.Chest;
-import org.bukkit.block.DoubleChest;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
-import org.bukkit.inventory.InventoryHolder;
-import org.bukkit.plugin.Plugin;
-import java.util.Objects;
+import static dev.w1zzrd.invtweaks.listener.PlayerMoveRenderListener.RENDER_RADIUS;
+import static dev.w1zzrd.spigot.wizcompat.command.CommandUtils.assertTrue;
-import static dev.w1zzrd.spigot.wizcompat.command.CommandUtils.*;
-import static dev.w1zzrd.spigot.wizcompat.packet.EntityCreator.*;
+public final class NamedChestCommand implements CommandExecutor {
-public class NamedChestCommand implements CommandExecutor {
+ private final NamedChestManager manager;
- private final Plugin plugin;
-
- public NamedChestCommand(final Plugin plugin) {
- this.plugin = plugin;
+ public NamedChestCommand(final NamedChestManager manager) {
+ this.manager = manager;
}
@Override
@@ -31,10 +25,7 @@ public class NamedChestCommand implements CommandExecutor {
if (assertTrue(sender instanceof Player && ((Player) sender).isOnline(), "Command can only be run by a player!", sender))
return true;
- if (assertTrue(args.length != 0, "Expected a name for the chest", sender))
- return true;
-
- if (assertTrue(args.length == 1, "Too many arguments for command", sender))
+ if (assertTrue(args.length <= 1, "Too many arguments for command", sender))
return true;
final Player player = (Player) sender;
@@ -44,43 +35,18 @@ public class NamedChestCommand implements CommandExecutor {
if (assertTrue(block != null && (block.getType() == Material.CHEST || block.getType() == Material.TRAPPED_CHEST), "You must be targeting a chest", sender))
return true;
- final Location loc = getCenterChestLocation(block);
+ final Chest chest = (Chest) block.getState();
- final Object entity = createFakeSlime(player);
- setSlimeSize(entity, 1);
+ if (args.length == 0) {
+ manager.removeTag(chest);
+ } else {
+ if (manager.hasNamedChest(chest))
+ manager.removeTag(chest);
- setEntityCollision(entity, false);
- setEntityCustomName(entity, args[0]);
- setEntityInvulnerable(entity, true);
- setEntityLocation(entity, loc.getX(), loc.getY(), loc.getZ(), 0f, 0f);
- setEntityCustomNameVisible(entity, true);
-
- sendEntitySpawnPacket(player, entity);
- sendEntityMetadataPacket(player, entity);
-
- final int entityID = getEntityID(entity);
-
- Bukkit.getScheduler().runTaskLater(plugin, () -> {
- sendEntityDespawnPacket(player, entityID);
- }, 60);
+ manager.addTag(chest, args[0]);
+ }
+ manager.renderTags(chest.getChunk(), RENDER_RADIUS);
return true;
}
-
- private static Location getCenterChestLocation(final Block chestBlock) {
- final InventoryHolder holder = Objects.requireNonNull(((Chest) chestBlock.getState()).getBlockInventory().getHolder()).getInventory().getHolder();
-
- if (holder instanceof final DoubleChest dChest) {
- final Location left = getBlockCenter(Objects.requireNonNull((Chest)dChest.getLeftSide()).getBlock());
- final Location right = getBlockCenter(Objects.requireNonNull((Chest)dChest.getRightSide()).getBlock());
-
- return new Location(left.getWorld(), (left.getX() + right.getX()) / 2.0, left.getY() + 0.2, (left.getZ() + right.getZ()) / 2.0);
- } else {
- return getBlockCenter(chestBlock).add(0.0, 0.2, 0.0);
- }
- }
-
- private static Location getBlockCenter(final Block block) {
- return block.getLocation().add(0.5, 0, 0.5);
- }
}
diff --git a/src/dev/w1zzrd/invtweaks/feature/NamedChestManager.java b/src/dev/w1zzrd/invtweaks/feature/NamedChestManager.java
new file mode 100644
index 0000000..aac82e9
--- /dev/null
+++ b/src/dev/w1zzrd/invtweaks/feature/NamedChestManager.java
@@ -0,0 +1,469 @@
+package dev.w1zzrd.invtweaks.feature;
+
+import dev.w1zzrd.invtweaks.serialization.ChestNameConfig;
+import dev.w1zzrd.spigot.wizcompat.serialization.PersistentData;
+import org.bukkit.Bukkit;
+import org.bukkit.Chunk;
+import org.bukkit.Location;
+import org.bukkit.World;
+import org.bukkit.block.Block;
+import org.bukkit.block.Chest;
+import org.bukkit.entity.Player;
+
+import java.util.*;
+import java.util.stream.Stream;
+
+import static dev.w1zzrd.spigot.wizcompat.block.Chests.*;
+import static dev.w1zzrd.spigot.wizcompat.packet.EntityCreator.*;
+
+public final class NamedChestManager {
+ private static final String PATH_NAMED_CHESTS = "namedChests";
+
+ private final RenderRegistry renders = new RenderRegistry();
+
+ private final ChestNameConfig config;
+
+ public NamedChestManager(final PersistentData data) {
+ config = data.loadData(PATH_NAMED_CHESTS, ChestNameConfig::new);
+ }
+
+ public boolean hasNamedChest(final World world, final Location loc) {
+ return getChestNameAt(world, loc) != null;
+ }
+
+ public boolean hasNamedChest(final Location loc) {
+ return hasNamedChest(Objects.requireNonNull(loc.getWorld()), loc);
+ }
+
+ public boolean hasNamedChest(final Chest chest) {
+ final Block left = getLeftChest(chest);
+
+ return hasNamedChest(left.getWorld(), left.getLocation());
+ }
+
+ private String getChestName(final Block block) {
+ return getChestNameAt(block.getWorld(), getLeftChest((Chest)block.getState()).getLocation());
+ }
+
+ private void setChestName(final Block block, final String name) {
+ addChestName(block.getWorld(), block.getLocation(), name);
+ }
+
+ private void addChestName(final World world, final Location location, final String name) {
+ config.getEntry(world.getUID()).add(location, name);
+ }
+
+ public String getChestNameAt(final World world, final Location location) {
+ return config.getEntry(world.getUID()).getName(location);
+ }
+
+ public String getChestNameAt(final Location location) {
+ return getChestNameAt(Objects.requireNonNull(location.getWorld()), location);
+ }
+
+ public void untrackPlayer(final Player player) {
+ renders.removeRender(player.getUniqueId());
+ }
+
+ public void renderTags(final Chunk chunk, final int chunkRadius) {
+ chunk.getWorld()
+ .getPlayers()
+ .stream()
+ .filter(it -> {
+ final Chunk playerChunk = it.getLocation().getChunk();
+
+ return Math.abs(playerChunk.getX() - chunk.getX()) <= chunkRadius && Math.abs(playerChunk.getZ() - chunk.getZ()) <= chunkRadius;
+ })
+ .forEach(player -> renderTags(player, chunkRadius));
+
+ final ChestNameConfig.ChestNameWorldEntry.ChestNameChunkEntry chestChunk = config.getChunkEntry(
+ chunk.getWorld().getUID(),
+ chunk.getX(),
+ chunk.getZ()
+ );
+
+ if (chestChunk != null)
+ chestChunk.setDirty(false);
+ }
+
+ public void renderTags(final Player target, final int chunkRadius) {
+ final UUID worldID = target.getWorld().getUID();
+
+ renders.updateRenders(
+ target,
+ chunkRadius,
+ addedChunk -> {
+ final ChestNameConfig.ChestNameWorldEntry.ChestNameChunkEntry chunk = config.getChunkEntry(worldID, addedChunk.getRender().x(), addedChunk.getRender().z());
+ if (chunk == null)
+ return;
+
+ final int baseX = chunk.getX() << 4;
+ final int baseZ = chunk.getZ() << 4;
+
+ chunk.streamEntries().forEach(entry -> {
+ final Object entity = entry.getEntity(() -> {
+ final Location loc = getCenterChestLocation(target.getWorld().getBlockAt(baseX + entry.getChunkX(), entry.getY(), baseZ + entry.getChunkZ()));
+
+ final Object newEntity = createFakeSlime(target);
+ setEntityCollision(newEntity, false);
+ setEntityInvulnerable(newEntity, true);
+ setEntityLocation(newEntity, loc.getX(), loc.getY(), loc.getZ(), 0f, 0f);
+ setEntityCustomName(newEntity, entry.getName());
+ setEntityCustomNameVisible(newEntity, true);
+
+ return newEntity;
+ });
+
+ sendEntitySpawnPacket(target, entity);
+ sendEntityMetadataPacket(target, entity);
+ });
+ },
+ removedChunk -> {
+ final ChestNameConfig.ChestNameWorldEntry.ChestNameChunkEntry chunk = config.getChunkEntry(worldID, removedChunk.getRender().x(), removedChunk.getRender().z());
+ if (chunk == null)
+ return;
+
+ chunk.streamEntries().forEach(entry -> {
+ final Object entity = entry.getEntity(() -> null);
+
+ if (entity != null)
+ sendEntityDespawnPacket(target, getEntityID(entity));
+ });
+ });
+ }
+
+ public void removeTag(final World world, final Location location) {
+ final ChestNameConfig.ChestNameWorldEntry.ChestNameChunkEntry.ChestNameConfigEntry entry = config.getEntryAt(world.getUID(), location);
+
+ if (entry != null) {
+ final ChunkRenderEntry chunk = renders.getChunk(location.getChunk().getX(), location.getChunk().getZ(), false);
+
+ if (chunk != null) {
+ final ChestNameConfig.ChestNameWorldEntry.ChestNameChunkEntry chestChunk = config.getChunkEntry(world.getUID(), chunk.getRender().x(), chunk.getRender().z());
+
+ if (chestChunk != null)
+ chestChunk.removeEntry(entry);
+
+ chunk.streamRenders().forEach(it -> {
+ final Player player = Bukkit.getPlayer(it.getRender());
+
+ if (player == null)
+ renders.removeRender(it.getRender());
+ else
+ sendEntityDespawnPacket(player, getEntityID(entry.getEntity(() -> null)));
+ });
+
+ if (chestChunk != null) {
+ chestChunk.setDirty(false);
+
+ // Make sure we don't leave blank data in persistent data file
+ final ChestNameConfig.ChestNameWorldEntry worldEntry = config.getEntry(world.getUID());
+ worldEntry.deleteEmptyChunk(chestChunk);
+ config.deleteEmptyWorld(worldEntry);
+ }
+ }
+ }
+ }
+
+ public void removeTag(final Location location) {
+ removeTag(Objects.requireNonNull(location.getWorld()), location);
+ }
+
+ public void addTag(final World world, final Location location, final String name) {
+ config.getEntry(world.getUID(), true).add(location, name);
+ }
+
+ public void addTag(final Chest chest, final String name) {
+ final Block left = getLeftChest(chest);
+
+ addTag(left.getWorld(), left.getLocation(), name);
+ }
+
+ public void removeTag(final Chest chest) {
+ final Block left = getLeftChest(chest);
+
+ removeTag(left.getWorld(), left.getLocation());
+ }
+
+ private static Location getCenterChestLocation(final Block chestBlock) {
+ if (isDoubleChest(chestBlock)) {
+ final Location left = getBlockCenter(getLeftChest((Chest) chestBlock.getState()));
+ final Location right = getBlockCenter(getRightChest((Chest) chestBlock.getState()));
+
+ return new Location(left.getWorld(), (left.getX() + right.getX()) / 2.0, left.getY() + 0.2, (left.getZ() + right.getZ()) / 2.0);
+ } else {
+ return getBlockCenter(chestBlock).add(0.0, 0.2, 0.0);
+ }
+ }
+
+ private static Location getBlockCenter(final Block block) {
+ return block.getLocation().add(0.5, 0, 0.5);
+ }
+
+ private final class RenderRegistry {
+ private final List playerRegistry = new ArrayList<>();
+ private final List chunkRegistry = new ArrayList<>();
+
+ public void addRender(final Player target, final int chunkX, final int chunkZ) {
+ final PlayerRenderEntry player = getPlayer(target.getUniqueId(), true);
+ final ChunkRenderEntry chunk = getChunk(chunkX, chunkZ, true);
+
+ assert player != null;
+ player.addRender(chunk);
+
+ assert chunk != null;
+ chunk.addRender(player);
+ }
+
+ public void removeRender(final Player target) {
+ removeRender(target.getUniqueId());
+ }
+
+ void removeRender(final UUID offlinePlayer) {
+ final PlayerRenderEntry player = getPlayer(offlinePlayer, false);
+
+ if (player != null) {
+ player.streamRenders().forEach(this::doRemoveChunk);
+ doRemovePlayer(player);
+ }
+ }
+
+ public void removeRender(final int chunkX, final int chunkZ) {
+ final ChunkRenderEntry chunk = getChunk(chunkX, chunkZ, false);
+
+ if (chunk != null) {
+ chunk.streamRenders().forEach(this::doRemovePlayer);
+ doRemoveChunk(chunk);
+ }
+ }
+
+ private void doRemoveChunk(final ChunkRenderEntry chunk) {
+ final int index = Collections.binarySearch(chunkRegistry, chunk);
+
+ if (index >= 0)
+ chunkRegistry.remove(index);
+ }
+
+ private void doRemovePlayer(final PlayerRenderEntry player) {
+ final int index = Collections.binarySearch(playerRegistry, player);
+
+ if (index >= 0)
+ playerRegistry.remove(index);
+ }
+
+ public void updateRenders(final Player target, final int chunkRadius, final ChunkEntryChangeHandler onAdd, final ChunkEntryChangeHandler onRemove) {
+ final PlayerRenderEntry player = getPlayer(target.getUniqueId(), true);
+ final UUID worldID = target.getWorld().getUID();
+ final int chunkX = target.getLocation().getBlockX() >> 4;
+ final int chunkZ = target.getLocation().getBlockZ() >> 4;
+
+ final int xMax = chunkX + chunkRadius;
+ final int xMin = chunkX - chunkRadius;
+ final int zMax = chunkZ + chunkRadius;
+ final int zMin = chunkZ - chunkRadius;
+
+ assert player != null;
+ final List toRemove = new ArrayList<>();
+ player.streamRenders()
+ .filter(chunk -> {
+ final ChestNameConfig.ChestNameWorldEntry.ChestNameChunkEntry chestChunk = config.getChunkEntry(worldID, chunk.getRender().x(), chunk.getRender().z());
+
+ return chunk.getRender().x() < xMax ||
+ chunk.getRender().x() < xMin ||
+ chunk.getRender().z() > zMax ||
+ chunk.getRender().z() < zMin ||
+ (chestChunk != null && chestChunk.isDirty());
+ }
+ )
+ .forEach(chunk -> {
+ toRemove.add(chunk);
+ onRemove.onChange(chunk);
+ });
+ toRemove.forEach(player::removeRender);
+
+ for (int x = xMin; x <= xMax; ++x)
+ for (int z = zMin; z <= zMax; ++z) {
+ final ChunkRenderEntry chunk = getChunk(x, z, true);
+
+ assert chunk != null;
+ if (player.addRender(chunk))
+ onAdd.onChange(chunk);
+
+ chunk.addRender(player);
+ }
+ }
+
+ public Stream streamPlayers() {
+ return playerRegistry.stream();
+ }
+
+ public Stream streamEntries(final Player target) {
+ final PlayerRenderEntry player = getPlayer(target.getUniqueId(), false);
+ final UUID worldID = target.getWorld().getUID();
+
+ if (player == null)
+ return null;
+ else
+ return player.streamRenders()
+ .map(chunkEntry -> config.getChunkEntry(worldID, chunkEntry.getRender().x(), chunkEntry.getRender().z()));
+ }
+
+ private ChunkRenderEntry getChunk(final int chunkX, final int chunkZ, final boolean addIfMissing) {
+ final ChunkRenderEntry find = new ChunkRenderEntry(chunkX, chunkZ);
+
+ final int index = Collections.binarySearch(chunkRegistry, find);
+
+ if (index >= 0)
+ return chunkRegistry.get(index);
+ else if (addIfMissing) {
+ chunkRegistry.add(-(index + 1), find);
+ return find;
+ }
+
+ return null;
+ }
+
+ private PlayerRenderEntry getPlayer(final UUID player, final boolean addIfMissing) {
+ final PlayerRenderEntry find = new PlayerRenderEntry(player);
+
+ final int index = Collections.binarySearch(playerRegistry, find);
+
+ if (index >= 0)
+ return playerRegistry.get(index);
+ else if (addIfMissing) {
+ playerRegistry.add(-(index + 1), find);
+ return find;
+ }
+
+ return null;
+ }
+ }
+
+
+ private interface RenderEntry, R> extends Comparable> {
+ T getRender();
+ boolean addRender(final R r);
+ boolean removeRender(final R r);
+ boolean containsRender(final R r);
+ Stream streamRenders();
+
+ @Override
+ default int compareTo(final RenderEntry o) {
+ return getRender().compareTo(o.getRender());
+ }
+ }
+
+ private static final class PlayerRenderEntry implements RenderEntry {
+ private final UUID player;
+ private final List chunks = new ArrayList<>();
+
+ public PlayerRenderEntry(final UUID player) {
+ this.player = player;
+ }
+
+ @Override
+ public UUID getRender() {
+ return player;
+ }
+
+ @Override
+ public boolean addRender(final ChunkRenderEntry chunk) {
+ final int index = Collections.binarySearch(chunks, chunk);
+
+ if (index < 0) {
+ chunks.add(-(index + 1), chunk);
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean removeRender(final ChunkRenderEntry chunk) {
+ final int index = Collections.binarySearch(chunks, chunk);
+
+ if (index >= 0) {
+ chunks.remove(index);
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean containsRender(final ChunkRenderEntry chunkRenderEntry) {
+ return Collections.binarySearch(chunks, chunkRenderEntry) >= 0;
+ }
+
+ @Override
+ public Stream streamRenders() {
+ return chunks.stream();
+ }
+ }
+
+
+ private static final class ChunkRenderEntry implements RenderEntry {
+ private final ChunkCoordinate coords;
+ private final List players = new ArrayList<>();
+
+ public ChunkRenderEntry(final int chunkX, final int chunkZ) {
+ coords = new ChunkCoordinate(chunkX, chunkZ);
+ }
+
+
+ @Override
+ public ChunkCoordinate getRender() {
+ return coords;
+ }
+
+ @Override
+ public boolean addRender(final PlayerRenderEntry player) {
+ final int index = Collections.binarySearch(players, player);
+
+ if (index < 0) {
+ players.add(-(index + 1), player);
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean removeRender(final PlayerRenderEntry player) {
+ final int index = Collections.binarySearch(players, player);
+
+ if (index >= 0) {
+ players.remove(index);
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean containsRender(final PlayerRenderEntry chunkRenderEntry) {
+ return Collections.binarySearch(players, chunkRenderEntry) >= 0;
+ }
+
+ @Override
+ public Stream streamRenders() {
+ return players.stream();
+ }
+
+ public record ChunkCoordinate(int x, int z) implements Comparable {
+ @Override
+ public int compareTo(final ChunkCoordinate o) {
+ final int compX = Integer.compare(x, o.x);
+
+ if (compX == 0)
+ return Integer.compare(z, o.z);
+
+ return compX;
+ }
+ }
+ }
+
+ private interface ChunkEntryChangeHandler {
+ void onChange(final ChunkRenderEntry chunk);
+ }
+}
diff --git a/src/dev/w1zzrd/invtweaks/listener/ChestBreakListener.java b/src/dev/w1zzrd/invtweaks/listener/ChestBreakListener.java
new file mode 100644
index 0000000..821488a
--- /dev/null
+++ b/src/dev/w1zzrd/invtweaks/listener/ChestBreakListener.java
@@ -0,0 +1,43 @@
+package dev.w1zzrd.invtweaks.listener;
+
+import dev.w1zzrd.invtweaks.feature.NamedChestManager;
+import org.bukkit.Material;
+import org.bukkit.block.Block;
+import org.bukkit.block.Chest;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.block.BlockBreakEvent;
+import org.bukkit.event.block.BlockEvent;
+import org.bukkit.event.block.BlockPlaceEvent;
+
+import static dev.w1zzrd.invtweaks.listener.PlayerMoveRenderListener.RENDER_RADIUS;
+import static dev.w1zzrd.spigot.wizcompat.block.Chests.getLeftChest;
+
+public final class ChestBreakListener implements Listener {
+
+ private final NamedChestManager manager;
+
+ public ChestBreakListener(final NamedChestManager manager) {
+ this.manager = manager;
+ }
+
+ @EventHandler
+ public void onChestBreak(final BlockBreakEvent event) {
+ if (!event.isCancelled())
+ processBlockEvent(event);
+ }
+
+ @EventHandler
+ public void onChestPlace(final BlockPlaceEvent event) {
+ if (!event.isCancelled())
+ processBlockEvent(event);
+ }
+
+ private void processBlockEvent(final BlockEvent event) {
+ if (event.getBlock().getType() == Material.CHEST || event.getBlock().getType() == Material.TRAPPED_CHEST) {
+ final Chest chest = (Chest) event.getBlock().getState();
+ manager.removeTag(chest);
+ manager.renderTags(chest.getChunk(), RENDER_RADIUS);
+ }
+ }
+}
diff --git a/src/dev/w1zzrd/invtweaks/listener/PlayerMoveRenderListener.java b/src/dev/w1zzrd/invtweaks/listener/PlayerMoveRenderListener.java
new file mode 100644
index 0000000..3ca2b9b
--- /dev/null
+++ b/src/dev/w1zzrd/invtweaks/listener/PlayerMoveRenderListener.java
@@ -0,0 +1,73 @@
+package dev.w1zzrd.invtweaks.listener;
+
+import dev.w1zzrd.invtweaks.feature.NamedChestManager;
+import org.bukkit.Chunk;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.player.PlayerJoinEvent;
+import org.bukkit.event.player.PlayerMoveEvent;
+import org.bukkit.event.player.PlayerQuitEvent;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
+
+public final class PlayerMoveRenderListener implements Listener {
+ public static final int RENDER_RADIUS = 3;
+
+ private final List trackers = new ArrayList<>();
+ private final List tracked = new ArrayList<>();
+ private final NamedChestManager manager;
+
+ public PlayerMoveRenderListener(final NamedChestManager manager) {
+ this.manager = manager;
+ }
+
+ @EventHandler
+ public void onPlayerMove(final PlayerMoveEvent event) {
+ final int index = Collections.binarySearch(tracked, event.getPlayer().getUniqueId());
+ final Player who = event.getPlayer();
+ final Chunk chunk = who.getLocation().getChunk();
+
+ if (index < 0) {
+ final int actualIndex = -(index + 1);
+ trackers.add(actualIndex, chunk);
+ tracked.add(actualIndex, who.getUniqueId());
+ triggerRender(who);
+ }
+ else if (!trackers.get(index).equals(event.getPlayer().getLocation().getChunk())) {
+ trackers.set(index, chunk);
+ triggerRender(event.getPlayer());
+ }
+ }
+
+ @EventHandler
+ public void onPlayerJoin(final PlayerJoinEvent event) {
+ final int index = Collections.binarySearch(tracked, event.getPlayer().getUniqueId());
+
+ // Should always be true
+ if (index < 0) {
+ trackers.add(-(index + 1), event.getPlayer().getLocation().getChunk());
+ tracked.add(-(index + 1), event.getPlayer().getUniqueId());
+ triggerRender(event.getPlayer());
+ }
+ }
+
+ @EventHandler
+ public void onPlayerLeave(final PlayerQuitEvent event) {
+ final int index = Collections.binarySearch(tracked, event.getPlayer().getUniqueId());
+
+ // Should always be true
+ if (index >= 0) {
+ trackers.remove(index);
+ tracked.remove(index);
+ manager.untrackPlayer(event.getPlayer());
+ }
+ }
+
+ private void triggerRender(final Player player) {
+ manager.renderTags(player, RENDER_RADIUS);
+ }
+}
diff --git a/src/dev/w1zzrd/invtweaks/serialization/ChestNameConfig.java b/src/dev/w1zzrd/invtweaks/serialization/ChestNameConfig.java
index 88f1d59..6414dd7 100644
--- a/src/dev/w1zzrd/invtweaks/serialization/ChestNameConfig.java
+++ b/src/dev/w1zzrd/invtweaks/serialization/ChestNameConfig.java
@@ -1,19 +1,367 @@
package dev.w1zzrd.invtweaks.serialization;
import dev.w1zzrd.spigot.wizcompat.serialization.SimpleReflectiveConfigItem;
+import org.bukkit.Location;
-import java.util.Map;
+import java.util.*;
+import java.util.stream.Stream;
public class ChestNameConfig extends SimpleReflectiveConfigItem {
- private Map locs;
+ private List worldEntries;
- /**
- * Required constructor for deserializing data
- *
- * @param mappings Data to deserialize
- */
public ChestNameConfig(Map mappings) {
super(mappings);
}
+
+ public ChestNameConfig() {
+ this(Collections.emptyMap());
+ worldEntries = new ArrayList<>();
+ }
+
+ public ChestNameWorldEntry getEntry(final UUID worldID, final boolean addIfMissing) {
+ final int index = indexOf(worldID);
+
+ if (index >= 0)
+ return worldEntries.get(index);
+ else if (addIfMissing) {
+ final ChestNameWorldEntry entry = new ChestNameWorldEntry(worldID);
+ worldEntries.add(-(index + 1), entry);
+ return entry;
+ }
+
+ return null;
+ }
+
+ public ChestNameWorldEntry getEntry(final UUID worldID) {
+ return getEntry(worldID, true);
+ }
+
+ public ChestNameWorldEntry.ChestNameChunkEntry getChunkEntry(final UUID worldID, final int chunkX, final int chunkZ) {
+ final int index = indexOf(worldID);
+
+ if (index >= 0)
+ return worldEntries.get(index).getChunk(chunkX, chunkZ);
+
+ return null;
+ }
+
+ public ChestNameWorldEntry.ChestNameChunkEntry.ChestNameConfigEntry getEntryAt(final UUID worldID, final Location location) {
+ final ChestNameWorldEntry.ChestNameChunkEntry chunk = getChunkEntry(worldID, location.getBlockX() >> 4, location.getBlockZ() >> 4);
+
+ if (chunk != null) {
+ return chunk.getEntry(location.getBlockX(), location.getBlockY(), location.getBlockZ(), false);
+ }
+
+ return null;
+ }
+
+ public boolean contains(final UUID worldID) {
+ return getEntry(worldID) != null;
+ }
+
+ public void add(final UUID worldID) {
+ getEntry(worldID);
+ }
+
+ public void remove(final UUID worldID) {
+ final int index = indexOf(worldID);
+
+ if (index >= 0)
+ worldEntries.remove(index);
+ }
+
+ private int indexOf(final UUID worldID) {
+ return Collections.binarySearch(worldEntries, new ChestNameWorldEntry(worldID));
+ }
+
+ public void deleteEmptyWorld(final ChestNameWorldEntry world) {
+ if (!world.hasEntries()) {
+ final int index = Collections.binarySearch(worldEntries, world);
+
+ if (index >= 0)
+ worldEntries.remove(index);
+ }
+ }
+
+
+ public static final class ChestNameWorldEntry extends SimpleReflectiveConfigItem implements Comparable {
+ private String worldIDStr;
+ private final transient UUID worldID;
+ private List chunks;
+
+ /**
+ * Required constructor for deserializing data
+ *
+ * @param mappings Data to deserialize
+ */
+ public ChestNameWorldEntry(Map mappings) {
+ super(mappings);
+ worldID = UUID.fromString(worldIDStr);
+ }
+
+ ChestNameWorldEntry(final UUID worldID) {
+ super(Collections.emptyMap());
+ this.worldID = worldID;
+ worldIDStr = worldID.toString();
+ chunks = new ArrayList<>();
+ }
+
+ public UUID getWorldID() {
+ return worldID;
+ }
+
+ @Override
+ public int compareTo(final ChestNameWorldEntry o) {
+ return getWorldID().compareTo(o.getWorldID());
+ }
+
+ public String getName(final Location location) {
+ final ChestNameChunkEntry chunk = getChunk(location, false);
+
+ if (chunk == null)
+ return null;
+ else
+ return chunk.getName(location);
+ }
+
+ public boolean contains(final Location location) {
+ return getName(location) != null;
+ }
+
+ public boolean hasEntries() {
+ return chunks.size() > 0;
+ }
+
+ public void add(final Location location, final String name) {
+ Objects.requireNonNull(getChunk(location, true)).add(location, name);
+ }
+
+ public void remove(final Location location) {
+ final int index = indexOf(location);
+
+ if (index >= 0)
+ chunks.remove(index);
+ }
+
+ private int indexOf(final Location location) {
+ return Collections.binarySearch(chunks, new ChestNameChunkEntry(location));
+ }
+
+ private int indexOf(final int chunkX, final int chunkZ) {
+ return Collections.binarySearch(chunks, new ChestNameChunkEntry(chunkX, chunkZ));
+ }
+
+ private ChestNameChunkEntry getChunk(final Location location, final boolean addIfMissing) {
+ final int index = indexOf(location);
+
+ if (index >= 0)
+ return chunks.get(index);
+ else if (addIfMissing) {
+ final ChestNameChunkEntry entry = new ChestNameChunkEntry(location);
+ chunks.add(-(index + 1), entry);
+ return entry;
+ }
+
+ return null;
+ }
+
+ private ChestNameChunkEntry getChunk(final int chunkX, final int chunkZ) {
+ final int index = indexOf(chunkX, chunkZ);
+
+ if (index >= 0)
+ return chunks.get(index);
+ else
+ return null;
+ }
+
+ private void clearChunk(final Location location) {
+ final int index = indexOf(location);
+
+ if (index >= 0)
+ chunks.remove(index);
+ }
+
+ public void deleteEmptyChunk(final ChestNameChunkEntry chunk) {
+ if (!chunk.hasEntries()) {
+ final int index = Collections.binarySearch(chunks, chunk);
+
+ if (index >= 0)
+ chunks.remove(index);
+ }
+ }
+
+ public static final class ChestNameChunkEntry extends SimpleReflectiveConfigItem implements Comparable {
+ private int x, z;
+ private List entries;
+ private transient boolean dirty = false;
+
+ public ChestNameChunkEntry(Map mappings) {
+ super(mappings);
+ }
+
+ ChestNameChunkEntry(final int x, final int z) {
+ super(Collections.emptyMap());
+ this.x = x;
+ this.z = z;
+ entries = new ArrayList<>();
+ }
+
+ ChestNameChunkEntry(final Location location) {
+ // Convert world coordinates to chunk coordinates
+ this(location.getBlockX() >> 4, location.getBlockZ() >> 4);
+ }
+
+ public int getX() {
+ return x;
+ }
+
+ public int getZ() {
+ return z;
+ }
+
+ public boolean isDirty() {
+ return dirty;
+ }
+
+ public void setDirty(final boolean dirty) {
+ this.dirty = dirty;
+ }
+
+ public boolean hasEntries() {
+ return entries.size() > 0;
+ }
+
+ public void add(final int x, final int y, final int z, final String name) {
+ final ChestNameConfigEntry check = getEntry(x, y, z, true);
+
+ assert check != null;
+ check.setName(name);
+
+ setDirty(true);
+ }
+
+ public void add(final Location location, final String name) {
+ add(location.getBlockX(), location.getBlockY(), location.getBlockZ(), name);
+ }
+
+ public String getName(final Location location) {
+ final ChestNameConfigEntry entry = getEntry(location.getBlockX(), location.getBlockY(), location.getBlockZ(), false);
+
+ if (entry == null)
+ return null;
+ else
+ return entry.getName();
+ }
+
+ private ChestNameConfigEntry getEntry(final int x, final int y, final int z, final boolean createIfMissing) {
+ final ChestNameConfigEntry find = new ChestNameConfigEntry(x, y, z);
+
+ final int index = indexOf(find);
+
+ if (index >= 0)
+ return entries.get(index);
+ else if (createIfMissing) {
+ entries.add(-(index + 1), find);
+ return find;
+ }
+
+ return null;
+ }
+
+ public void removeEntry(final ChestNameConfigEntry entry) {
+ final int index = indexOf(entry);
+
+ if (index >= 0) {
+ entries.remove(index);
+ setDirty(true);
+ }
+ }
+
+ public Stream streamEntries() {
+ return entries.stream();
+ }
+
+ private int indexOf(final ChestNameConfigEntry find) {
+ return Collections.binarySearch(entries, find);
+ }
+
+ @Override
+ public int compareTo(final ChestNameChunkEntry o) {
+ final int compX = Integer.compare(x, o.x);
+
+ if (compX == 0)
+ return Integer.compare(z, o.z);
+
+ return compX;
+ }
+
+ public static final class ChestNameConfigEntry extends SimpleReflectiveConfigItem implements Comparable {
+ private transient Object entity;
+ private transient int locInt;
+ private String loc;
+ private String name;
+
+ public ChestNameConfigEntry(Map mappings) {
+ super(mappings);
+ locInt = Integer.parseInt(loc, 16);
+ }
+
+ ChestNameConfigEntry(final int x, final int y, final int z, final String name) {
+ super(Collections.emptyMap());
+ locInt = ((y & 0xFFF) << 8) | ((x & 0xF) << 4) | (z & 0xF);
+ loc = Integer.toString(locInt, 16);
+ this.name = name;
+ }
+
+ ChestNameConfigEntry(final int x, final int y, final int z) {
+ this(x, y, z, null);
+ }
+
+ ChestNameConfigEntry(final Location location, final String name) {
+ this(location.getBlockX() & 0xF, location.getBlockY(), location.getBlockZ() & 0xF, name);
+ }
+
+ ChestNameConfigEntry(final Location location) {
+ this(location, null);
+ }
+
+ public Object getEntity(final EntityCreator creator) {
+ if (entity == null)
+ entity = creator.createFakeEntity();
+
+ return entity;
+ }
+
+ public int getChunkX() {
+ return (locInt >>> 4) & 0xF;
+ }
+
+ public int getChunkZ() {
+ return locInt & 0xF;
+ }
+
+ public int getY() {
+ return (locInt >>> 8) & 0xFFF;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ void setName(final String name) {
+ this.name = name;
+ }
+
+ @Override
+ public int compareTo(final ChestNameConfigEntry o) {
+ return Integer.compare(locInt, o.locInt);
+ }
+
+ public interface EntityCreator {
+ Object createFakeEntity();
+ }
+ }
+ }
+ }
}