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(); + } + } + } + } }