diff --git a/build.gradle.kts b/build.gradle.kts index 50947f2..04f4b46 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -74,7 +74,12 @@ tasks { hangar("ViaVersion", "5.0.1") hangar("ViaBackwards", "5.0.1") hangar("PlaceholderAPI", "2.11.6") + + // For testing groups in config.yml url("https://download.luckperms.net/1556/bukkit/loader/LuckPerms-Bukkit-5.4.141.jar") + + // For testing TAB hook + github("NEZNAMY", "TAB", "4.1.8", "TAB.v4.1.8.jar") } } } diff --git a/src/main/java/com/mattmx/nametags/EventsListener.java b/src/main/java/com/mattmx/nametags/EventsListener.java index a930b85..3fc25d7 100644 --- a/src/main/java/com/mattmx/nametags/EventsListener.java +++ b/src/main/java/com/mattmx/nametags/EventsListener.java @@ -18,9 +18,11 @@ public class EventsListener implements Listener { @EventHandler public void onPlayerJoin(@NotNull PlayerJoinEvent event) { - NameTags.getInstance() + final NameTagEntity tag = NameTags.getInstance() .getEntityManager() .getOrCreateNameTagEntity(event.getPlayer()); + + tag.updateVisibility(); } @EventHandler diff --git a/src/main/java/com/mattmx/nametags/NameTags.java b/src/main/java/com/mattmx/nametags/NameTags.java index bc4a38e..3dfaecd 100644 --- a/src/main/java/com/mattmx/nametags/NameTags.java +++ b/src/main/java/com/mattmx/nametags/NameTags.java @@ -1,8 +1,10 @@ package com.mattmx.nametags; import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.PacketEventsAPI; import com.mattmx.nametags.config.ConfigDefaultsListener; import com.mattmx.nametags.entity.NameTagEntityManager; +import com.mattmx.nametags.hook.NeznamyTABHook; import me.tofaa.entitylib.APIConfig; import me.tofaa.entitylib.EntityLib; import me.tofaa.entitylib.spigot.SpigotEntityLibPlatform; @@ -45,9 +47,11 @@ public void onEnable() { EntityLib.init(platform, settings); - PacketEvents.getAPI() - .getEventManager() - .registerListener(packetListener); + final PacketEventsAPI packetEvents = PacketEvents.getAPI(); + + packetEvents.getEventManager().registerListener(packetListener); + + NeznamyTABHook.inject(this); Bukkit.getPluginManager().registerEvents(eventsListener, this); diff --git a/src/main/java/com/mattmx/nametags/OutgoingPacketListener.java b/src/main/java/com/mattmx/nametags/OutgoingPacketListener.java index 0d07aa6..92b8d8c 100644 --- a/src/main/java/com/mattmx/nametags/OutgoingPacketListener.java +++ b/src/main/java/com/mattmx/nametags/OutgoingPacketListener.java @@ -3,8 +3,10 @@ import com.github.retrooper.packetevents.event.PacketListenerAbstract; import com.github.retrooper.packetevents.event.PacketSendEvent; import com.github.retrooper.packetevents.protocol.packettype.PacketType; -import com.github.retrooper.packetevents.protocol.world.Location; +import com.github.retrooper.packetevents.protocol.potion.PotionTypes; import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerDestroyEntities; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityEffect; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerRemoveEntityEffect; import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerSpawnEntity; import com.mattmx.nametags.entity.NameTagEntity; import org.jetbrains.annotations.NotNull; @@ -51,6 +53,28 @@ public void onPacketSend(@NotNull PacketSendEvent event) { nameTagEntity.getPassenger().removeViewer(event.getUser()); } } + case PacketType.Play.Server.ENTITY_EFFECT -> { + final WrapperPlayServerEntityEffect packet = new WrapperPlayServerEntityEffect(event); + + if (packet.getPotionType() != PotionTypes.INVISIBILITY) return; + + final NameTagEntity nameTagEntity = plugin.getEntityManager().getNameTagEntityById(packet.getEntityId()); + + if (nameTagEntity == null) return; + + nameTagEntity.updateVisibility(true); + } + case PacketType.Play.Server.REMOVE_ENTITY_EFFECT -> { + final WrapperPlayServerRemoveEntityEffect packet = new WrapperPlayServerRemoveEntityEffect(event); + + if (packet.getPotionType() != PotionTypes.INVISIBILITY) return; + + final NameTagEntity nameTagEntity = plugin.getEntityManager().getNameTagEntityById(packet.getEntityId()); + + if (nameTagEntity == null) return; + + nameTagEntity.updateVisibility(false); + } default -> { } } diff --git a/src/main/java/com/mattmx/nametags/config/ConfigDefaultsListener.java b/src/main/java/com/mattmx/nametags/config/ConfigDefaultsListener.java index b8af5a2..4154f5d 100644 --- a/src/main/java/com/mattmx/nametags/config/ConfigDefaultsListener.java +++ b/src/main/java/com/mattmx/nametags/config/ConfigDefaultsListener.java @@ -3,6 +3,7 @@ import com.mattmx.nametags.NameTags; import com.mattmx.nametags.entity.trait.RefreshTrait; import com.mattmx.nametags.event.NameTagEntityCreateEvent; +import me.tofaa.entitylib.meta.display.AbstractDisplayMeta; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; @@ -51,6 +52,13 @@ public void onCreate(@NotNull NameTagEntityCreateEvent event) { TextDisplayMetaConfiguration.applyTextMeta(e.getValue(), entity.getMeta(), player, player); }); + entity.updateVisibility(); + + if (entity.getMeta().getBillboardConstraints() == AbstractDisplayMeta.BillboardConstraints.CENTER) { + // Look passenger down to remove debug getting in the way + entity.getPassenger().rotateHead(0f, 90f); + } + entity.getPassenger().refresh(); } ) diff --git a/src/main/java/com/mattmx/nametags/config/ConfigHelper.java b/src/main/java/com/mattmx/nametags/config/ConfigHelper.java new file mode 100644 index 0000000..c5ed5b8 --- /dev/null +++ b/src/main/java/com/mattmx/nametags/config/ConfigHelper.java @@ -0,0 +1,48 @@ +package com.mattmx.nametags.config; + +import me.tofaa.entitylib.meta.display.AbstractDisplayMeta; +import org.bukkit.configuration.ConfigurationSection; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; + +public class ConfigHelper { + + public static @Nullable T takeIfPresent( + @NotNull ConfigurationSection section, + @NotNull String key, + @NotNull Function provider, + @NotNull Consumer take + ) { + Optional optional = Optional.ofNullable(provider.apply(key)); + + optional.ifPresent(take); + + return optional.orElse(null); + } + + public static @Nullable T takeIfPresentOrElse( + @NotNull ConfigurationSection section, + @NotNull String key, + @NotNull Function provider, + @NotNull Consumer take, + @NotNull Runnable orElse + ) { + Optional optional = Optional.ofNullable(provider.apply(key)); + + optional.ifPresentOrElse(take, orElse); + + return optional.orElse(null); + } + + public static > @Nullable T getEnumByNameOrNull(Class enums, @NotNull String name) { + return Arrays.stream(enums.getEnumConstants()) + .filter((e) -> e.name().equalsIgnoreCase(name)) + .findFirst().orElse(null); + } + +} diff --git a/src/main/java/com/mattmx/nametags/config/TextDisplayMetaConfiguration.java b/src/main/java/com/mattmx/nametags/config/TextDisplayMetaConfiguration.java index b8c1cc9..1f70f02 100644 --- a/src/main/java/com/mattmx/nametags/config/TextDisplayMetaConfiguration.java +++ b/src/main/java/com/mattmx/nametags/config/TextDisplayMetaConfiguration.java @@ -1,5 +1,6 @@ package com.mattmx.nametags.config; +import com.github.retrooper.packetevents.util.Vector3f; import com.mattmx.nametags.NameTags; import com.mattmx.nametags.hook.PapiHook; import me.clip.placeholderapi.PlaceholderAPI; @@ -19,24 +20,25 @@ public class TextDisplayMetaConfiguration { - public static void applyTextMeta(@NotNull ConfigurationSection section, @NotNull TextDisplayMeta to, @NotNull Player self, @NotNull Player sender) { + public static boolean applyTextMeta(@NotNull ConfigurationSection section, @NotNull TextDisplayMeta to, @NotNull Player self, @NotNull Player sender) { Component text = section.getStringList("text") .stream() .map((line) -> convertToComponent(self, sender, line)) .reduce((a, b) -> a.append(Component.newline()).append(b)) .orElse(null); - if (text == null) return; + if (text == null) return false; if (!text.equals(to.getText())) { to.setText(text); + return true; } + return false; } public static void applyMeta(@NotNull ConfigurationSection section, @NotNull TextDisplayMeta to) { - String backgroundColor = section.getString("background"); - if (backgroundColor != null) { + ConfigHelper.takeIfPresent(section, "background", section::getString, (backgroundColor) -> { int background; if (backgroundColor.equalsIgnoreCase("transparent")) { @@ -50,43 +52,110 @@ public static void applyMeta(@NotNull ConfigurationSection section, @NotNull Tex } to.setBackgroundColor(background); - } + }); + + ConfigHelper.takeIfPresent(section, "billboard", section::getString, (billboardString) -> { + AbstractDisplayMeta.BillboardConstraints billboard = ConfigHelper.getEnumByNameOrNull( + AbstractDisplayMeta.BillboardConstraints.class, + billboardString.toLowerCase(Locale.ROOT) + ); + + Objects.requireNonNull(billboard, "Unknown billboard type in section " + section.getCurrentPath() + " named " + billboardString); - String billboardString = section.getString("billboard"); - if (billboardString != null) { - AbstractDisplayMeta.BillboardConstraints billboard = AbstractDisplayMeta.BillboardConstraints.valueOf(billboardString.toUpperCase(Locale.ROOT)); if (billboard != to.getBillboardConstraints()) { to.setBillboardConstraints(billboard); } - } + }); - String shadow = section.getString("shadow"); - if (shadow != null) { - if (!shadow.equalsIgnoreCase(Boolean.toString(to.isShadow()))) { - to.setShadow(Boolean.parseBoolean(shadow)); + // TODO(matt): impl other features + ConfigHelper.takeIfPresent(section, "see-through", section::getBoolean, (seeThrough) -> { + if (to.isSeeThrough() != seeThrough) { + to.setSeeThrough(seeThrough); } - } + }); - String range = section.getString("range"); - if (range != null) { - float finalRange = range.equalsIgnoreCase("default") - ? (Bukkit.getSimulationDistance() * 16f) - : Float.parseFloat(range); + ConfigHelper.takeIfPresent(section, "line-width", section::getInt, (lineWidth) -> { + if (to.getLineWidth() != lineWidth) { + to.setLineWidth(lineWidth); + } + }); - if (finalRange != to.getViewRange()) { - to.setViewRange(finalRange); + ConfigHelper.takeIfPresent(section, "text-opacity", section::getInt, (opacity) -> { + byte byteValue = opacity.byteValue(); + if (to.getTextOpacity() != byteValue) { + to.setTextOpacity(byteValue); } - } + }); + + ConfigHelper.takeIfPresent(section, "text-shadow", section::getBoolean, (shadow) -> { + if (to.isShadow() != shadow) { + to.setShadow(shadow); + } + }); + + ConfigHelper.takeIfPresent(section, "translate", section::getConfigurationSection, (vector) -> { + double dx = vector.getDouble("x"); + double dy = vector.getDouble("y"); + double dz = vector.getDouble("z"); - String gap = section.getString("gap"); - if (gap != null) { + Vector3f vec = new Vector3f((float) dx, (float) dy, (float) dz); + if (!Objects.equals(to.getTranslation(), vec)) { + to.setTranslation(vec); + } + }); + + ConfigHelper.takeIfPresent(section, "gap", section::getString, (gap) -> { float finalGap = gap.equalsIgnoreCase("default") ? 0.2f : Float.parseFloat(gap); if (finalGap != to.getTranslation().y) { to.setTranslation(to.getTranslation().withY(finalGap)); } - } + }); + + ConfigHelper.takeIfPresent(section, "scale", section::getConfigurationSection, (vector) -> { + double dx = vector.getDouble("x"); + double dy = vector.getDouble("y"); + double dz = vector.getDouble("z"); + + Vector3f vec = new Vector3f((float) dx, (float) dy, (float) dz); + if (!Objects.equals(to.getScale(), vec)) { + to.setScale(vec); + } + }); + + ConfigHelper.takeIfPresent(section, "brightness", section::getInt, (brightness) -> { + if (to.getBrightnessOverride() != brightness) { + to.setBrightnessOverride(brightness); + } + }); + + ConfigHelper.takeIfPresent(section, "shadow", section::getConfigurationSection, (shadow) -> { + float strength = (float) shadow.getDouble("strength"); + float radius = (float) shadow.getDouble("radius"); + + if (to.getShadowStrength() != strength) { + to.setShadowStrength(strength); + } + if (to.getShadowRadius() != radius) { + to.setShadowRadius(radius); + } + + }); + + ConfigHelper.takeIfPresent(section, "range", section::getString, (range) -> { + float finalRange = range.equalsIgnoreCase("default") + ? (Bukkit.getSimulationDistance() * 16f) + : Float.parseFloat(range); + + if (finalRange != to.getViewRange()) { + to.setViewRange(finalRange); + } + }); + + // TODO(matt): This function needs access to the WrapperEntity + String yaw = section.getString("yaw"); + String pitch = section.getString("pitch"); } private static Component convertToComponent(Player self, Player sending, String line) { diff --git a/src/main/java/com/mattmx/nametags/entity/NameTagEntity.java b/src/main/java/com/mattmx/nametags/entity/NameTagEntity.java index 3d62b5f..d1e7246 100644 --- a/src/main/java/com/mattmx/nametags/entity/NameTagEntity.java +++ b/src/main/java/com/mattmx/nametags/entity/NameTagEntity.java @@ -11,8 +11,10 @@ import me.tofaa.entitylib.meta.display.TextDisplayMeta; import me.tofaa.entitylib.wrapper.WrapperEntity; import org.bukkit.entity.Entity; +import org.bukkit.entity.LivingEntity; import org.bukkit.entity.Player; import org.bukkit.entity.TextDisplay; +import org.bukkit.potion.PotionEffectType; import org.jetbrains.annotations.NotNull; import java.util.function.Consumer; @@ -21,6 +23,7 @@ public class NameTagEntity { private final @NotNull TraitHolder traits = new TraitHolder(this); private final @NotNull Entity bukkitEntity; private final @NotNull WrapperEntity passenger; + private float cachedViewRange = -1f; public NameTagEntity(@NotNull Entity entity) { this.bukkitEntity = entity; @@ -45,6 +48,28 @@ public void initialize() { } } + public boolean isInvisible() { + boolean hasInvisibilityEffect = bukkitEntity instanceof LivingEntity e + && e.hasPotionEffect(PotionEffectType.INVISIBILITY); + + return bukkitEntity.isInvisible() || hasInvisibilityEffect; + } + + public void updateVisibility() { + updateVisibility(isInvisible()); + } + + public void updateVisibility(final boolean isInvisible) { + modify((meta) -> { + if (isInvisible && !meta.isInvisible()) { + this.cachedViewRange = meta.getViewRange(); + meta.setViewRange(0f); + } else if (!isInvisible && meta.isInvisible()) { + meta.setViewRange(this.cachedViewRange); + } + }); + } + public @NotNull TraitHolder getTraits() { return traits; } diff --git a/src/main/java/com/mattmx/nametags/hook/NeznamyTABHook.java b/src/main/java/com/mattmx/nametags/hook/NeznamyTABHook.java index f2aa698..ea26700 100644 --- a/src/main/java/com/mattmx/nametags/hook/NeznamyTABHook.java +++ b/src/main/java/com/mattmx/nametags/hook/NeznamyTABHook.java @@ -2,14 +2,19 @@ import com.mattmx.nametags.NameTags; import me.neznamy.tab.api.TabAPI; +import me.neznamy.tab.api.TabPlayer; +import me.neznamy.tab.api.event.player.PlayerLoadEvent; import me.neznamy.tab.api.nametag.NameTagManager; import me.neznamy.tab.api.nametag.UnlimitedNameTagManager; import org.bukkit.Bukkit; import org.jetbrains.annotations.NotNull; +import java.util.Objects; + public class NeznamyTABHook { public static void inject(@NotNull NameTags plugin) { + // Execute on first tick since we don't know when TAB will be available. Bukkit.getScheduler().runTask(plugin, NeznamyTABHook::start); } @@ -18,12 +23,38 @@ private static void start() { if (!isTab) return; + NameTags plugin = NameTags.getInstance(); NameTagManager nameTagManager = TabAPI.getInstance().getNameTagManager(); - if (nameTagManager instanceof UnlimitedNameTagManager unlimitedNameTagManager) { - // TODO(matt): Disable this module somehow? - // Maybe we need to use the TAB jar as a dependency, i don't think api exposes it. + if (nameTagManager instanceof UnlimitedNameTagManager) { + plugin.getLogger().warning(""" + ⚠ TAB UnlimitedNameTags Mode detected! ⚠ + + DisplayNameTags will attempt to disable this module however + we strongly recommend disabling it in TAB's config. + + This is because both TAB UNT mode and DisplayNameTags attempt + to use Passengers to sync positions of custom name tags. + Having both could cause some visual issues in-game. + + Furthermore, the UnlimitedNameTags module is deprecated and + will be removed in 5.0.0 + + Read more at https://gist.github.com/NEZNAMY/f4cabf2fd9251a836b5eb877720dee5c + + """); + } else { + plugin.getLogger().info("Attempting to override TAB's name tags"); } + + Objects.requireNonNull(TabAPI.getInstance().getEventBus()) + .register(PlayerLoadEvent.class, (event) -> { + final TabPlayer tabPlayer = event.getPlayer(); + + TabAPI.getInstance() + .getNameTagManager() + .hideNameTag(tabPlayer); + }); } } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index cd27786..e8f675e 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -14,7 +14,7 @@ defaults: - "%player_ping%ms" background: black billboard: center - shadow: true + text-shadow: true range: default gap: 0.2 @@ -25,4 +25,9 @@ groups: mvp: background: green staff: - background: red \ No newline at end of file + background: red + +# Extra features +extra: + # Should the name-tag be see-through if the player is glowing? + glowing-feature: true \ No newline at end of file