diff --git a/common/src/main/java/com/hibiscusmc/hmccosmetics/HMCCosmeticsPlugin.java b/common/src/main/java/com/hibiscusmc/hmccosmetics/HMCCosmeticsPlugin.java index d0a26de3..b93033ff 100644 --- a/common/src/main/java/com/hibiscusmc/hmccosmetics/HMCCosmeticsPlugin.java +++ b/common/src/main/java/com/hibiscusmc/hmccosmetics/HMCCosmeticsPlugin.java @@ -18,15 +18,14 @@ import com.hibiscusmc.hmccosmetics.hooks.misc.HookBetterHud; import com.hibiscusmc.hmccosmetics.hooks.placeholders.HMCPlaceholderExpansion; import com.hibiscusmc.hmccosmetics.hooks.worldguard.WGHook; import com.hibiscusmc.hmccosmetics.hooks.worldguard.WGListener; -import com.hibiscusmc.hmccosmetics.listener.PaperPlayerGameListener; -import com.hibiscusmc.hmccosmetics.listener.PlayerConnectionListener; -import com.hibiscusmc.hmccosmetics.listener.PlayerGameListener; -import com.hibiscusmc.hmccosmetics.listener.ServerListener; +import com.hibiscusmc.hmccosmetics.listener.*; import com.hibiscusmc.hmccosmetics.packets.CosmeticPacketInterface; import com.hibiscusmc.hmccosmetics.user.CosmeticUser; import com.hibiscusmc.hmccosmetics.user.CosmeticUsers; +import com.hibiscusmc.hmccosmetics.user.manager.UserSearchManager; import com.hibiscusmc.hmccosmetics.util.MessagesUtil; import com.hibiscusmc.hmccosmetics.util.TranslationUtil; +import lombok.Getter; import me.lojosho.hibiscuscommons.HibiscusCommonsPlugin; import me.lojosho.hibiscuscommons.HibiscusPlugin; import me.lojosho.hibiscuscommons.config.serializer.ItemSerializer; @@ -51,6 +50,9 @@ public final class HMCCosmeticsPlugin extends HibiscusPlugin { private static HMCCosmeticsPlugin instance; private static YamlConfigurationLoader configLoader; + @Getter + private UserSearchManager userSearchManager; + public HMCCosmeticsPlugin() { super(13873, 1879); new HookHMCCosmetics(); @@ -62,6 +64,9 @@ public final class HMCCosmeticsPlugin extends HibiscusPlugin { // Plugin startup logic instance = this; + // Search Service + this.userSearchManager = new UserSearchManager(); + // File setup saveDefaultConfig(); if (!Path.of(getDataFolder().getPath(), "messages.yml").toFile().exists()) saveResource("messages.yml", false); @@ -102,6 +107,8 @@ public final class HMCCosmeticsPlugin extends HibiscusPlugin { getServer().getPluginManager().registerEvents(new PlayerConnectionListener(), this); getServer().getPluginManager().registerEvents(new PlayerGameListener(), this); getServer().getPluginManager().registerEvents(new ServerListener(), this); + getServer().getPluginManager().registerEvents(new PlayerMovementListener(), this); + getServer().getPluginManager().registerEvents(userSearchManager, this); if (HibiscusCommonsPlugin.isOnPaper()) { getServer().getPluginManager().registerEvents(new PaperPlayerGameListener(), this); diff --git a/common/src/main/java/com/hibiscusmc/hmccosmetics/cosmetic/Cosmetic.java b/common/src/main/java/com/hibiscusmc/hmccosmetics/cosmetic/Cosmetic.java index a98e9d8f..5ede62d3 100644 --- a/common/src/main/java/com/hibiscusmc/hmccosmetics/cosmetic/Cosmetic.java +++ b/common/src/main/java/com/hibiscusmc/hmccosmetics/cosmetic/Cosmetic.java @@ -1,5 +1,6 @@ package com.hibiscusmc.hmccosmetics.cosmetic; +import com.hibiscusmc.hmccosmetics.cosmetic.behavior.CosmeticUpdateBehavior; import com.hibiscusmc.hmccosmetics.user.CosmeticUser; import com.hibiscusmc.hmccosmetics.util.MessagesUtil; import lombok.AccessLevel; @@ -104,16 +105,21 @@ public abstract class Cosmetic { * Dispatched when an update is requested upon the cosmetic. * @param user the user to preform the update against */ - public final void update(CosmeticUser user) { - this.doUpdate(user); + @Deprecated(since = "2.8.2") + public void update(CosmeticUser user) { + if(this instanceof CosmeticUpdateBehavior behavior) { + behavior.dispatchUpdate(user); + } } /** * Action preformed on the update. * @param user the user to preform the update against */ + @Deprecated(since = "2.8.2") protected void doUpdate(final CosmeticUser user) { - // NO-OP. + // #update should be the preferred way of interacting with this api now. + this.update(user); } @Nullable diff --git a/common/src/main/java/com/hibiscusmc/hmccosmetics/cosmetic/behavior/CosmeticMovementBehavior.java b/common/src/main/java/com/hibiscusmc/hmccosmetics/cosmetic/behavior/CosmeticMovementBehavior.java new file mode 100644 index 00000000..81a78e97 --- /dev/null +++ b/common/src/main/java/com/hibiscusmc/hmccosmetics/cosmetic/behavior/CosmeticMovementBehavior.java @@ -0,0 +1,12 @@ +package com.hibiscusmc.hmccosmetics.cosmetic.behavior; + +import com.hibiscusmc.hmccosmetics.user.CosmeticUser; +import org.bukkit.Location; + +public interface CosmeticMovementBehavior { + void dispatchMove( + final CosmeticUser user, + final Location from, + final Location to + ); +} diff --git a/common/src/main/java/com/hibiscusmc/hmccosmetics/cosmetic/behavior/CosmeticUpdateBehavior.java b/common/src/main/java/com/hibiscusmc/hmccosmetics/cosmetic/behavior/CosmeticUpdateBehavior.java new file mode 100644 index 00000000..5ca6995e --- /dev/null +++ b/common/src/main/java/com/hibiscusmc/hmccosmetics/cosmetic/behavior/CosmeticUpdateBehavior.java @@ -0,0 +1,7 @@ +package com.hibiscusmc.hmccosmetics.cosmetic.behavior; + +import com.hibiscusmc.hmccosmetics.user.CosmeticUser; + +public interface CosmeticUpdateBehavior { + void dispatchUpdate(final CosmeticUser user); +} \ No newline at end of file diff --git a/common/src/main/java/com/hibiscusmc/hmccosmetics/cosmetic/types/CosmeticArmorType.java b/common/src/main/java/com/hibiscusmc/hmccosmetics/cosmetic/types/CosmeticArmorType.java index 71cb6a90..890acf4f 100644 --- a/common/src/main/java/com/hibiscusmc/hmccosmetics/cosmetic/types/CosmeticArmorType.java +++ b/common/src/main/java/com/hibiscusmc/hmccosmetics/cosmetic/types/CosmeticArmorType.java @@ -2,6 +2,7 @@ package com.hibiscusmc.hmccosmetics.cosmetic.types; import com.hibiscusmc.hmccosmetics.config.Settings; import com.hibiscusmc.hmccosmetics.cosmetic.Cosmetic; +import com.hibiscusmc.hmccosmetics.cosmetic.behavior.CosmeticUpdateBehavior; import com.hibiscusmc.hmccosmetics.user.CosmeticUser; import com.hibiscusmc.hmccosmetics.util.HMCCInventoryUtils; import com.hibiscusmc.hmccosmetics.util.packets.HMCCPacketManager; @@ -15,8 +16,7 @@ import org.bukkit.inventory.EquipmentSlot; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; -public class CosmeticArmorType extends Cosmetic { - +public class CosmeticArmorType extends Cosmetic implements CosmeticUpdateBehavior { private final EquipmentSlot equipSlot; public CosmeticArmorType(String id, ConfigurationNode config) { @@ -31,7 +31,7 @@ public class CosmeticArmorType extends Cosmetic { } @Override - protected void doUpdate(@NotNull CosmeticUser user) { + public void dispatchUpdate(CosmeticUser user) { if (user.isInWardrobe()) return; Entity entity = Bukkit.getEntity(user.getUniqueId()); if (entity == null) return; diff --git a/common/src/main/java/com/hibiscusmc/hmccosmetics/cosmetic/types/CosmeticBackpackType.java b/common/src/main/java/com/hibiscusmc/hmccosmetics/cosmetic/types/CosmeticBackpackType.java index 6b5eb6d6..1b3837f3 100644 --- a/common/src/main/java/com/hibiscusmc/hmccosmetics/cosmetic/types/CosmeticBackpackType.java +++ b/common/src/main/java/com/hibiscusmc/hmccosmetics/cosmetic/types/CosmeticBackpackType.java @@ -2,21 +2,19 @@ package com.hibiscusmc.hmccosmetics.cosmetic.types; import com.hibiscusmc.hmccosmetics.config.Settings; import com.hibiscusmc.hmccosmetics.cosmetic.Cosmetic; +import com.hibiscusmc.hmccosmetics.cosmetic.behavior.CosmeticMovementBehavior; +import com.hibiscusmc.hmccosmetics.cosmetic.behavior.CosmeticUpdateBehavior; import com.hibiscusmc.hmccosmetics.user.CosmeticUser; import com.hibiscusmc.hmccosmetics.user.manager.UserBackpackManager; import com.hibiscusmc.hmccosmetics.user.manager.UserEntity; -import com.hibiscusmc.hmccosmetics.util.MessagesUtil; import com.hibiscusmc.hmccosmetics.util.packets.HMCCPacketManager; import lombok.Getter; -import me.lojosho.hibiscuscommons.nms.NMSHandlers; -import me.lojosho.hibiscuscommons.packets.BundledRidingData; import me.lojosho.hibiscuscommons.util.packets.PacketManager; import me.lojosho.shaded.configurate.ConfigurationNode; import org.bukkit.Location; import org.bukkit.attribute.Attribute; import org.bukkit.attribute.AttributeInstance; import org.bukkit.entity.Entity; -import org.bukkit.entity.EntityType; import org.bukkit.entity.Player; import org.bukkit.inventory.EquipmentSlot; import org.bukkit.inventory.ItemStack; @@ -26,9 +24,8 @@ import java.util.ArrayList; import java.util.List; import java.util.UUID; -public class CosmeticBackpackType extends Cosmetic { - - @Getter +@Getter +public class CosmeticBackpackType extends Cosmetic implements CosmeticUpdateBehavior, CosmeticMovementBehavior { private int height = -1; private ItemStack firstPersonBackpack; @@ -42,30 +39,24 @@ public class CosmeticBackpackType extends Cosmetic { } @Override - protected void doUpdate(@NotNull CosmeticUser user) { + public void dispatchUpdate(CosmeticUser user) { Entity entity = user.getEntity(); - if (entity == null) return; + if(entity == null) { + return; + } Location entityLocation = entity.getLocation(); Location loc = entityLocation.clone().add(0, 2, 0); - if (user.isInWardrobe() || !user.isBackpackSpawned()) return; - if (user.isHidden()) { - // Sometimes the backpack is not despawned when the player is hidden (weird ass logic happening somewhere) - user.despawnBackpack(); - return; - } - UserBackpackManager backpackManager = user.getUserBackpackManager(); + if(backpackManager == null) return; + UserEntity entityManager = backpackManager.getEntityManager(); int firstArmorStandId = backpackManager.getFirstArmorStandId(); List newViewers = entityManager.refreshViewers(loc); - entityManager.teleport(loc); - entityManager.setRotation((int) loc.getYaw(), isFirstPersonCompadible()); - - if (!newViewers.isEmpty()) { + if(!newViewers.isEmpty()) { HMCCPacketManager.spawnInvisibleArmorstand(firstArmorStandId, entityLocation, UUID.randomUUID(), newViewers); PacketManager.equipmentSlotUpdate(firstArmorStandId, EquipmentSlot.HEAD, user.getUserCosmeticItem(this, getItem()), newViewers); @@ -101,11 +92,26 @@ public class CosmeticBackpackType extends Cosmetic { backpackManager.showBackpack(); } + @Override + public void dispatchMove(CosmeticUser user, Location from, Location to) { + @SuppressWarnings("DuplicatedCode") // thanks. + Entity entity = user.getEntity(); + if(entity == null) { + return; + } + + Location entityLocation = entity.getLocation(); + Location loc = entityLocation.clone().add(0, 2, 0); + + UserBackpackManager backpackManager = user.getUserBackpackManager(); + UserEntity entityManager = backpackManager.getEntityManager(); + + entityManager.teleport(loc); + entityManager.setRotation((int) loc.getYaw(), isFirstPersonCompadible()); + } + public boolean isFirstPersonCompadible() { return firstPersonBackpack != null; } - public ItemStack getFirstPersonBackpack() { - return firstPersonBackpack; - } } diff --git a/common/src/main/java/com/hibiscusmc/hmccosmetics/cosmetic/types/CosmeticBalloonType.java b/common/src/main/java/com/hibiscusmc/hmccosmetics/cosmetic/types/CosmeticBalloonType.java index f31d4907..20a65d2d 100644 --- a/common/src/main/java/com/hibiscusmc/hmccosmetics/cosmetic/types/CosmeticBalloonType.java +++ b/common/src/main/java/com/hibiscusmc/hmccosmetics/cosmetic/types/CosmeticBalloonType.java @@ -2,6 +2,8 @@ package com.hibiscusmc.hmccosmetics.cosmetic.types; import com.hibiscusmc.hmccosmetics.config.Settings; import com.hibiscusmc.hmccosmetics.cosmetic.Cosmetic; +import com.hibiscusmc.hmccosmetics.cosmetic.behavior.CosmeticMovementBehavior; +import com.hibiscusmc.hmccosmetics.cosmetic.behavior.CosmeticUpdateBehavior; import com.hibiscusmc.hmccosmetics.user.CosmeticUser; import com.hibiscusmc.hmccosmetics.user.manager.UserBalloonManager; import com.hibiscusmc.hmccosmetics.util.MessagesUtil; @@ -14,11 +16,10 @@ import org.bukkit.Location; import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import org.bukkit.util.Vector; -import org.jetbrains.annotations.NotNull; import java.util.List; -public class CosmeticBalloonType extends Cosmetic { +public class CosmeticBalloonType extends Cosmetic implements CosmeticUpdateBehavior, CosmeticMovementBehavior { @Getter private final String modelName; @@ -54,7 +55,7 @@ public class CosmeticBalloonType extends Cosmetic { } @Override - protected void doUpdate(@NotNull CosmeticUser user) { + public void dispatchUpdate(CosmeticUser user) { Entity entity = Bukkit.getEntity(user.getUniqueId()); UserBalloonManager userBalloonManager = user.getBalloonManager(); @@ -66,6 +67,29 @@ public class CosmeticBalloonType extends Cosmetic { return; } + Location newLocation = entity.getLocation(); + newLocation = newLocation.clone().add(getBalloonOffset()); + if (Settings.isBalloonHeadForward()) newLocation.setPitch(0); + + if (!user.isHidden() && showLead) { + List sendTo = userBalloonManager.getPufferfish().refreshViewers(newLocation); + if (sendTo.isEmpty()) return; + user.getBalloonManager().getPufferfish().spawnPufferfish(newLocation, sendTo); + } + } + + @Override + public void dispatchMove(CosmeticUser user, Location from, Location to) { + Entity entity = Bukkit.getEntity(user.getUniqueId()); + UserBalloonManager userBalloonManager = user.getBalloonManager(); + + if (entity == null || userBalloonManager == null) return; + if (user.isInWardrobe()) return; + + if (!userBalloonManager.getModelEntity().isValid()) { + return; + } + Location newLocation = entity.getLocation(); Location currentLocation = user.getBalloonManager().getLocation(); newLocation = newLocation.clone().add(getBalloonOffset()); @@ -90,15 +114,6 @@ public class CosmeticBalloonType extends Cosmetic { HMCCPacketManager.sendTeleportPacket(userBalloonManager.getPufferfishBalloonId(), newLocation, false, viewer); HMCCPacketManager.sendLeashPacket(userBalloonManager.getPufferfishBalloonId(), entity.getEntityId(), viewer); - if (user.isHidden()) { - userBalloonManager.getPufferfish().hidePufferfish(); - return; - } - if (!user.isHidden() && showLead) { - List sendTo = userBalloonManager.getPufferfish().refreshViewers(newLocation); - if (sendTo.isEmpty()) return; - user.getBalloonManager().getPufferfish().spawnPufferfish(newLocation, sendTo); - } } public boolean isDyeablePart(String name) { diff --git a/common/src/main/java/com/hibiscusmc/hmccosmetics/listener/PlayerGameListener.java b/common/src/main/java/com/hibiscusmc/hmccosmetics/listener/PlayerGameListener.java index 0742e19f..f5fc0af7 100644 --- a/common/src/main/java/com/hibiscusmc/hmccosmetics/listener/PlayerGameListener.java +++ b/common/src/main/java/com/hibiscusmc/hmccosmetics/listener/PlayerGameListener.java @@ -40,7 +40,6 @@ import org.jetbrains.annotations.Nullable; import java.util.*; public class PlayerGameListener implements Listener { - @EventHandler(priority = EventPriority.LOW) public void onPlayerClick(@NotNull InventoryClickEvent event) { // || !event.getClickedInventory().getType().equals(InventoryType.PLAYER) @@ -159,15 +158,6 @@ public class PlayerGameListener implements Listener { } } - @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) - public void onPlayerMove(PlayerMoveEvent event) { - Player player = event.getPlayer(); - CosmeticUser user = CosmeticUsers.getUser(player); - if (user == null) return; - user.updateCosmetic(CosmeticSlot.BACKPACK); - user.updateCosmetic(CosmeticSlot.BALLOON); - } - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onPlayerPoseChange(EntityPoseChangeEvent event) { if (!(event.getEntity() instanceof Player player)) return; diff --git a/common/src/main/java/com/hibiscusmc/hmccosmetics/listener/PlayerMovementListener.java b/common/src/main/java/com/hibiscusmc/hmccosmetics/listener/PlayerMovementListener.java new file mode 100644 index 00000000..8491ca26 --- /dev/null +++ b/common/src/main/java/com/hibiscusmc/hmccosmetics/listener/PlayerMovementListener.java @@ -0,0 +1,95 @@ +package com.hibiscusmc.hmccosmetics.listener; + +import com.hibiscusmc.hmccosmetics.cosmetic.CosmeticSlot; +import com.hibiscusmc.hmccosmetics.user.CosmeticUser; +import com.hibiscusmc.hmccosmetics.user.CosmeticUsers; +import lombok.extern.slf4j.Slf4j; +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerChangedWorldEvent; +import org.bukkit.event.player.PlayerMoveEvent; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +@Slf4j +public class PlayerMovementListener implements Listener { + private static final List MOVEMENT_COSMETICS = List.of( + CosmeticSlot.BACKPACK, + CosmeticSlot.BALLOON + ); + + // Player Id -> Small Location + private final Map locations = new HashMap<>(); + + @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) + public void onPlayerMove(PlayerMoveEvent ev) { + final Player player = ev.getPlayer(); + + final CosmeticUser user = CosmeticUsers.getUser(player); + if(user == null) { + return; + } + + if(!updateDirtyLocation(ev.getPlayer(), ev.getTo())) { + return; + } + + for(final CosmeticSlot slot : MOVEMENT_COSMETICS) { + user.updateMovementCosmetic(slot, ev.getFrom(), ev.getTo()); + } + } + + private boolean updateDirtyLocation(final Player player, final Location nextLoc) { + final SmallLocation previous = locations.computeIfAbsent( + player.getUniqueId(), + $ -> SmallLocation.fromLocation(nextLoc) + ); + final SmallLocation next = SmallLocation.fromLocation(nextLoc); + + if(next.distanceTo(previous) > 0.25) { + this.locations.put(player.getUniqueId(), next); + return true; + } + + if(next.yawDistanceTo(previous) > 5) { + this.locations.put(player.getUniqueId(), next); + return true; + } + + return false; + } + + @EventHandler(priority = EventPriority.NORMAL) + public void onWorldChange(final PlayerChangedWorldEvent ev) { + this.locations.remove(ev.getPlayer().getUniqueId()); + } + + record SmallLocation( + double x, + double y, + double z, + float yaw + ) { + public double distanceTo(SmallLocation other) { + double dx = this.x - other.x; + double dy = this.y - other.y; + double dz = this.z - other.z; + return Math.sqrt(dx * dx + dy * dy + dz * dz); + } + + public float yawDistanceTo(SmallLocation other) { + float diff = Math.abs(this.yaw - other.yaw) % 360; + return diff > 180 ? 360 - diff : diff; + } + + public static SmallLocation fromLocation(final Location location) { + return new SmallLocation(location.getX(), location.getY(), location.getZ(), location.getYaw()); + } + } +} diff --git a/common/src/main/java/com/hibiscusmc/hmccosmetics/user/CosmeticUser.java b/common/src/main/java/com/hibiscusmc/hmccosmetics/user/CosmeticUser.java index eb21c4f6..09499b6d 100644 --- a/common/src/main/java/com/hibiscusmc/hmccosmetics/user/CosmeticUser.java +++ b/common/src/main/java/com/hibiscusmc/hmccosmetics/user/CosmeticUser.java @@ -10,6 +10,8 @@ import com.hibiscusmc.hmccosmetics.config.WardrobeSettings; import com.hibiscusmc.hmccosmetics.cosmetic.Cosmetic; import com.hibiscusmc.hmccosmetics.cosmetic.CosmeticHolder; import com.hibiscusmc.hmccosmetics.cosmetic.CosmeticSlot; +import com.hibiscusmc.hmccosmetics.cosmetic.behavior.CosmeticMovementBehavior; +import com.hibiscusmc.hmccosmetics.cosmetic.behavior.CosmeticUpdateBehavior; import com.hibiscusmc.hmccosmetics.cosmetic.types.CosmeticArmorType; import com.hibiscusmc.hmccosmetics.cosmetic.types.CosmeticBackpackType; import com.hibiscusmc.hmccosmetics.cosmetic.types.CosmeticBalloonType; @@ -28,6 +30,7 @@ import me.lojosho.hibiscuscommons.util.InventoryUtils; import me.lojosho.hibiscuscommons.util.packets.PacketManager; import org.bukkit.Bukkit; import org.bukkit.Color; +import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.entity.Entity; import org.bukkit.entity.HumanEntity; @@ -299,10 +302,31 @@ public class CosmeticUser implements CosmeticHolder { @Override public void updateCosmetic(@NotNull CosmeticSlot slot) { - Cosmetic cosmetic = playerCosmetics.get(slot); - if (cosmetic != null) { - cosmetic.update(this); + final Cosmetic cosmetic = playerCosmetics.get(slot); + if(cosmetic == null) { + return; } + + if(!(cosmetic instanceof CosmeticUpdateBehavior behavior)) { + throw new IllegalArgumentException("attempted to update a cosmetic that does not implement CosmeticUpdateBehavior, " + + "please ensure this cosmetic is properly allowed to update."); + } + + behavior.dispatchUpdate(this); + } + + public void updateMovementCosmetic(CosmeticSlot slot, final Location from, final Location to) { + final Cosmetic cosmetic = playerCosmetics.get(slot); + if(cosmetic == null) { + return; + } + + if(!(cosmetic instanceof CosmeticMovementBehavior behavior)) { + throw new IllegalArgumentException("attempted to update a cosmetic that does not implement CosmeticUpdateBehavior, " + + "please ensure this cosmetic is properly allowed to update."); + } + + behavior.dispatchMove(this, from, to); } public void updateCosmetic(Cosmetic cosmetic) { @@ -311,10 +335,15 @@ public class CosmeticUser implements CosmeticHolder { public void updateCosmetic() { MessagesUtil.sendDebugMessages("updateCosmetic (All) - start"); - HashMap items = new HashMap<>(); + final HashMap items = new HashMap<>(); - for (Cosmetic cosmetic : playerCosmetics.values()) { - if (cosmetic instanceof CosmeticArmorType armorType) { + for(final Cosmetic cosmetic : playerCosmetics.values()) { + if(!(cosmetic instanceof CosmeticUpdateBehavior behavior)) { + continue; + } + + // defers item updates to end of operation + if(cosmetic instanceof CosmeticArmorType armorType) { if (isInWardrobe()) return; if (!(getEntity() instanceof HumanEntity humanEntity)) return; @@ -325,12 +354,19 @@ public class CosmeticUser implements CosmeticHolder { items.put(HMCCInventoryUtils.getEquipmentSlot(armorType.getSlot()), armorType.getItem(this)); } else { - cosmetic.update(this); + behavior.dispatchUpdate(this); } } - if (items.isEmpty() || getEntity() == null) return; - PacketManager.equipmentSlotUpdate(getEntity().getEntityId(), items, HMCCPacketManager.getViewers(getEntity().getLocation())); - MessagesUtil.sendDebugMessages("updateCosmetic (All) - end - " + items.size()); + + final Entity entity = this.getEntity(); + if(!items.isEmpty() && entity != null) { + PacketManager.equipmentSlotUpdate( + entity.getEntityId(), + items, + HMCCPacketManager.getViewers(entity.getLocation()) + ); + MessagesUtil.sendDebugMessages("updateCosmetic (All) - end - " + items.size()); + } } public ItemStack getUserCosmeticItem(CosmeticSlot slot) { diff --git a/common/src/main/java/com/hibiscusmc/hmccosmetics/user/manager/UserEntity.java b/common/src/main/java/com/hibiscusmc/hmccosmetics/user/manager/UserEntity.java index 6fb4beb7..65f30855 100644 --- a/common/src/main/java/com/hibiscusmc/hmccosmetics/user/manager/UserEntity.java +++ b/common/src/main/java/com/hibiscusmc/hmccosmetics/user/manager/UserEntity.java @@ -1,5 +1,6 @@ package com.hibiscusmc.hmccosmetics.user.manager; +import com.hibiscusmc.hmccosmetics.HMCCosmeticsPlugin; import com.hibiscusmc.hmccosmetics.config.Settings; import com.hibiscusmc.hmccosmetics.user.CosmeticUser; import com.hibiscusmc.hmccosmetics.user.CosmeticUsers; @@ -12,10 +13,7 @@ import org.bukkit.Location; import org.bukkit.entity.Entity; import org.bukkit.entity.Player; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.UUID; +import java.util.*; public class UserEntity { @@ -41,7 +39,10 @@ public class UserEntity { } public List refreshViewers(Location location) { - if (System.currentTimeMillis() - viewerLastUpdate <= 1000) return List.of(); //Prevents mass refreshes + //Prevents mass refreshes + if(System.currentTimeMillis() - viewerLastUpdate <= 3000) { + return List.of(); + } Entity ownerPlayer = Bukkit.getEntity(owner); if (ownerPlayer == null) { @@ -49,36 +50,55 @@ public class UserEntity { return List.of(); } - final HashSet players = new HashSet<>(HMCCPacketManager.getViewers(location)); - final ArrayList newPlayers = new ArrayList<>(); - final ArrayList removePlayers = new ArrayList<>(); + final List players = HMCCosmeticsPlugin.getInstance() + .getUserSearchManager() + .getPlayersInRange(location, Settings.getViewDistance()); + + final ArrayList newPlayerIds = new ArrayList<>(); + final ArrayList removePlayerIds = new ArrayList<>(); // Go through all nearby players, check if they are new to the viewers list. for (Player player : players) { CosmeticUser user = CosmeticUsers.getUser(player); - if (user != null && owner != user.getUniqueId() && user.isInWardrobe() && !player.canSee(ownerPlayer)) { // Fixes issue where players in wardrobe would see other players cosmetics if they were not in wardrobe - removePlayers.add(player); + if( + user != null + && owner != user.getUniqueId() + && user.isInWardrobe() + // Fixes issue where players in wardrobe would see other players cosmetics if they were not in wardrobe + && !player.canSee(ownerPlayer) + ) { + removePlayerIds.add(player.getUniqueId()); continue; } + if (!viewers.contains(player)) { viewers.add(player); - newPlayers.add(player); + newPlayerIds.add(player.getUniqueId()); } } // Basically, if they are not nearby, they are still in the viewers and we need to kick em to the curb for (Player viewerPlayer : viewers) { if (!players.contains(viewerPlayer)) { - removePlayers.add(viewerPlayer); + removePlayerIds.add(viewerPlayer.getUniqueId()); } } // If there are players for removal, send the packets to them - if (!removePlayers.isEmpty()) { + if (!removePlayerIds.isEmpty()) { + final List removePlayers = removePlayerIds.stream() + .map(Bukkit::getPlayer) + .filter(Objects::nonNull) + .toList(); + HMCCPacketManager.sendEntityDestroyPacket(ids, removePlayers); viewers.removeAll(removePlayers); } - setViewerLastUpdate(System.currentTimeMillis()); - return newPlayers; + + this.setViewerLastUpdate(System.currentTimeMillis()); + return newPlayerIds.stream() + .map(Bukkit::getPlayer) + .filter(Objects::nonNull) + .toList(); } public void teleport(Location location) { diff --git a/common/src/main/java/com/hibiscusmc/hmccosmetics/user/manager/UserSearchManager.java b/common/src/main/java/com/hibiscusmc/hmccosmetics/user/manager/UserSearchManager.java new file mode 100644 index 00000000..239fbc95 --- /dev/null +++ b/common/src/main/java/com/hibiscusmc/hmccosmetics/user/manager/UserSearchManager.java @@ -0,0 +1,98 @@ +package com.hibiscusmc.hmccosmetics.user.manager; + +import com.hibiscusmc.hmccosmetics.util.Octree; +import org.bukkit.Location; +import org.bukkit.World; +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.*; + +public class UserSearchManager implements Listener { + private final Map> worldOctrees = new HashMap<>(); + private final Map playerPositions = new HashMap<>(); + + private static final double WORLD_HALF_SIZE = 30_000_000; + + private Octree getOrCreateOctree(World world) { + return worldOctrees.computeIfAbsent(world.getUID(), $ -> { + Octree.BoundingBox worldBoundary = new Octree.BoundingBox( + new Octree.Point3D(0, 160, 0), WORLD_HALF_SIZE + ); + return new Octree<>(worldBoundary); + }); + } + + private Octree.Point3D toPoint3D(Location location) { + return new Octree.Point3D(location.getX(), location.getY(), location.getZ()); + } + + public boolean addPlayer(Player player) { + Octree octree = getOrCreateOctree(player.getWorld()); + Octree.Point3D point = toPoint3D(player.getLocation()); + + if(octree.insert(point, player)) { + playerPositions.put(player.getUniqueId(), point); + return true; + } + return false; + } + + public boolean removePlayer(Player player) { + Octree octree = worldOctrees.get(player.getWorld().getUID()); + if (octree == null) return false; + + Octree.Point3D point = playerPositions.remove(player.getUniqueId()); + if (point != null) { + return octree.remove(point, player); + } + return false; + } + + public void updatePlayerPosition(Player player) { + removePlayer(player); + addPlayer(player); + } + + public List getPlayersInRange(Location location, double range) { + Octree octree = worldOctrees.get(location.getWorld().getUID()); + if (octree == null) { + return Collections.emptyList(); + } + + Octree.Point3D point = toPoint3D(location); + Octree.BoundingBox searchArea = new Octree.BoundingBox(point, range); + + return octree.queryRange(searchArea) + .stream() + .filter(Objects::nonNull) + .toList(); + } + + @EventHandler + public void onPlayerMove(PlayerMoveEvent event) { + Player player = event.getPlayer(); + if (event.hasChangedBlock()) { + updatePlayerPosition(player); + } + } + + public void clear() { + worldOctrees.clear(); + playerPositions.clear(); + } + + @EventHandler + public void onPlayerQuit(PlayerQuitEvent event) { + removePlayer(event.getPlayer()); + } + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + addPlayer(event.getPlayer()); + } +} \ No newline at end of file diff --git a/common/src/main/java/com/hibiscusmc/hmccosmetics/util/Octree.java b/common/src/main/java/com/hibiscusmc/hmccosmetics/util/Octree.java new file mode 100644 index 00000000..025769ff --- /dev/null +++ b/common/src/main/java/com/hibiscusmc/hmccosmetics/util/Octree.java @@ -0,0 +1,148 @@ +package com.hibiscusmc.hmccosmetics.util; + +import java.util.ArrayList; +import java.util.List; + +public class Octree { + + private static final int MAX_CAPACITY = 8; + private BoundingBox boundary; + private List points; + private Octree[] children; + private boolean isDivided; + + public Octree(BoundingBox boundary) { + this.boundary = boundary; + this.points = new ArrayList<>(); + this.isDivided = false; + } + + public boolean insert(Point3D position, T data) { + if (!boundary.contains(position)) { + return false; + } + + if (points.size() < MAX_CAPACITY && !isDivided) { + points.add(new Entry(position, data)); + return true; + } + + if (!isDivided) { + subdivide(); + } + + for (Octree child : children) { + if (child.insert(position, data)) { + return true; + } + } + + return false; + } + + public boolean remove(Point3D point, T player) { + if (!boundary.contains(point)) { + return false; + } + + if (points.size() < MAX_CAPACITY && !isDivided) { + points.removeIf(entry -> entry.position.equals(point) && entry.data.equals(player)); + return true; + } + + if (!isDivided) { + return false; + } + + for (Octree child : children) { + if (child.remove(point, player)) { + return true; + } + } + + return false; + } + + public List queryRange(BoundingBox range) { + List found = new ArrayList<>(); + + if (!boundary.intersects(range)) { + return found; + } + + for (Entry entry : points) { + if (range.contains(entry.position)) { + found.add(entry.data); + } + } + + if (isDivided) { + for (Octree child : children) { + found.addAll(child.queryRange(range)); + } + } + + return found; + } + + private void subdivide() { + double x = boundary.center.x; + double y = boundary.center.y; + double z = boundary.center.z; + double newHalfSize = boundary.halfSize / 2.0; + + children = new Octree[8]; + children[0] = new Octree<>(new BoundingBox(new Point3D(x - newHalfSize, y - newHalfSize, z - newHalfSize), newHalfSize)); + children[1] = new Octree<>(new BoundingBox(new Point3D(x + newHalfSize, y - newHalfSize, z - newHalfSize), newHalfSize)); + children[2] = new Octree<>(new BoundingBox(new Point3D(x - newHalfSize, y + newHalfSize, z - newHalfSize), newHalfSize)); + children[3] = new Octree<>(new BoundingBox(new Point3D(x + newHalfSize, y + newHalfSize, z - newHalfSize), newHalfSize)); + children[4] = new Octree<>(new BoundingBox(new Point3D(x - newHalfSize, y - newHalfSize, z + newHalfSize), newHalfSize)); + children[5] = new Octree<>(new BoundingBox(new Point3D(x + newHalfSize, y - newHalfSize, z + newHalfSize), newHalfSize)); + children[6] = new Octree<>(new BoundingBox(new Point3D(x - newHalfSize, y + newHalfSize, z + newHalfSize), newHalfSize)); + children[7] = new Octree<>(new BoundingBox(new Point3D(x + newHalfSize, y + newHalfSize, z + newHalfSize), newHalfSize)); + + isDivided = true; + } + + public static class Point3D { + double x, y, z; + + public Point3D(double x, double y, double z) { + this.x = x; + this.y = y; + this.z = z; + } + } + + public static class BoundingBox { + Point3D center; + double halfSize; + + public BoundingBox(Point3D center, double halfSize) { + this.center = center; + this.halfSize = halfSize; + } + + public boolean contains(Point3D point) { + return Math.abs(point.x - center.x) <= halfSize && + Math.abs(point.y - center.y) <= halfSize && + Math.abs(point.z - center.z) <= halfSize; + } + + public boolean intersects(BoundingBox other) { + return Math.abs(center.x - other.center.x) <= halfSize + other.halfSize && + Math.abs(center.y - other.center.y) <= halfSize + other.halfSize && + Math.abs(center.z - other.center.z) <= halfSize + other.halfSize; + } + } + + private class Entry { + Point3D position; + T data; + + Entry(Point3D position, T data) { + this.position = position; + this.data = data; + } + } +} \ No newline at end of file