diff --git a/NOTICE.md b/NOTICE.md index 8e8ec8f13..9fc666da2 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -50,3 +50,7 @@ The project includes software developed by third parties. For their full license - XGlow: - Repository: `Xezard/XGlow` - License: **Apache-2.0 License** (See `3rd_party_licenses/LICENSE-Apache_v2`) + +- packetevents: + - Repository: `retrooper/packetevents` + - License: **General Public License v3.0** (See `3rd_party_licenses/LICENSE-GPLv3`) diff --git a/buildSrc/src/main/java/dev/magicspells/gradle/MSJavaPlugin.java b/buildSrc/src/main/java/dev/magicspells/gradle/MSJavaPlugin.java index 7c89f6548..cf70d5165 100644 --- a/buildSrc/src/main/java/dev/magicspells/gradle/MSJavaPlugin.java +++ b/buildSrc/src/main/java/dev/magicspells/gradle/MSJavaPlugin.java @@ -24,19 +24,20 @@ public void apply(Project target) { repositories.mavenCentral(); String[] mavenUrls = new String[] { - "https://repo.dmulloy2.net/nexus/repository/public/", - "https://repo.md-5.net/content/repositories/releases/", - "https://repo.papermc.io/repository/maven-public/", - "https://repo.aikar.co/content/groups/aikar/", - "https://oss.sonatype.org/content/repositories/central", - "https://oss.sonatype.org/content/repositories/snapshots", - "https://hub.spigotmc.org/nexus/content/repositories/snapshots/", - "https://jitpack.io", - "https://repo.codemc.org/repository/maven-public", - "https://cdn.rawgit.com/Rayzr522/maven-repo/master/", - "https://maven.enginehub.org/repo/", - "https://repo.glaremasters.me/repository/towny/", - "https://repo.extendedclip.com/content/repositories/placeholderapi" + "https://repo.dmulloy2.net/nexus/repository/public/", + "https://repo.md-5.net/content/repositories/releases/", + "https://repo.papermc.io/repository/maven-public/", + "https://repo.aikar.co/content/groups/aikar/", + "https://oss.sonatype.org/content/repositories/central", + "https://oss.sonatype.org/content/repositories/snapshots", + "https://hub.spigotmc.org/nexus/content/repositories/snapshots/", + "https://jitpack.io", + "https://repo.codemc.org/repository/maven-public", + "https://cdn.rawgit.com/Rayzr522/maven-repo/master/", + "https://maven.enginehub.org/repo/", + "https://repo.glaremasters.me/repository/towny/", + "https://repo.extendedclip.com/content/repositories/placeholderapi", + "https://repo.md-5.net/content/repositories/snapshots", }; for (String url : mavenUrls) { repositories.maven(mavenArtifactRepository -> mavenArtifactRepository.setUrl(url)); diff --git a/core/build.gradle b/core/build.gradle index d0e17c3a0..37fd5328b 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -15,8 +15,9 @@ dependencies { shadow(project(path: ":nms:shared", configuration: "apiElements")) shadow(project(path: ":nms:latest")) { transitive = false } + implementation(group: "com.github.retrooper", name: "packetevents-spigot", version: "2.7.0") implementation(group: "com.comphenix.protocol", name: "ProtocolLib", version: "5.3.0") { transitive = false } - implementation(group: "com.github.libraryaddict", name: "LibsDisguises", version: "v10.0.25") { transitive = false } + implementation(group: "me.libraryaddict.disguises", name: "libsdisguises", version: "10.0.44-SNAPSHOT") { transitive = false } implementation(group: "net.milkbowl.vault", name: "VaultAPI", version: "1.7") { transitive = false } implementation(group: "me.clip", name: "placeholderapi", version: "2.11.6") { transitive = false } implementation(group: "com.github.GriefPrevention", name: "GriefPrevention", version: "17.0.0") { transitive = false } diff --git a/core/src/main/java/com/nisovin/magicspells/MagicSpells.java b/core/src/main/java/com/nisovin/magicspells/MagicSpells.java index 3f2862257..59bba02b5 100644 --- a/core/src/main/java/com/nisovin/magicspells/MagicSpells.java +++ b/core/src/main/java/com/nisovin/magicspells/MagicSpells.java @@ -19,6 +19,7 @@ import java.nio.file.Path; import java.nio.file.Files; +import com.google.common.collect.Sets; import com.google.common.collect.SetMultimap; import com.google.common.collect.LinkedHashMultimap; @@ -1869,9 +1870,13 @@ public static void registerEvents(final Listener listener) { public static void registerEvents(final Listener listener, EventPriority customPriority) { if (customPriority == null) customPriority = EventPriority.NORMAL; - Method[] methods; + Set methods; try { - methods = listener.getClass().getDeclaredMethods(); + Class listenerClazz = listener.getClass(); + methods = Sets.union( + Set.of(listenerClazz.getMethods()), + Set.of(listenerClazz.getDeclaredMethods()) + ); } catch (NoClassDefFoundError e) { DebugHandler.debugNoClassDefFoundError(e); return; diff --git a/core/src/main/java/com/nisovin/magicspells/Spell.java b/core/src/main/java/com/nisovin/magicspells/Spell.java index db7dc74ec..e93ada561 100644 --- a/core/src/main/java/com/nisovin/magicspells/Spell.java +++ b/core/src/main/java/com/nisovin/magicspells/Spell.java @@ -7,6 +7,7 @@ import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; import java.util.*; import java.util.function.Predicate; @@ -938,6 +939,14 @@ protected ConfigData getConfigDataRegistryEntry(@NotNull St return ConfigDataUtil.getRegistryEntry(config.getMainConfig(), internalKey + key, registry, def); } + protected ConfigData getConfigDataNamedTextColor(String key, NamedTextColor def) { + return ConfigDataUtil.getNamedTextColor(config.getMainConfig(), internalKey + key, def); + } + + protected ConfigData getConfigDataNamespacedKey(String key, NamespacedKey def) { + return ConfigDataUtil.getNamespacedKey(config.getMainConfig(), internalKey + key, def); + } + /** * @param key Path for the string or section format SpellFilter to be read from. */ diff --git a/core/src/main/java/com/nisovin/magicspells/spells/targeted/GlowSpell.java b/core/src/main/java/com/nisovin/magicspells/spells/targeted/GlowSpell.java new file mode 100644 index 000000000..e7a4cb1f9 --- /dev/null +++ b/core/src/main/java/com/nisovin/magicspells/spells/targeted/GlowSpell.java @@ -0,0 +1,114 @@ +package com.nisovin.magicspells.spells.targeted; + +import java.util.UUID; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.NamespacedKey; +import org.bukkit.entity.LivingEntity; + +import net.kyori.adventure.text.format.NamedTextColor; + +import com.nisovin.magicspells.MagicSpells; +import com.nisovin.magicspells.util.SpellData; +import com.nisovin.magicspells.util.CastResult; +import com.nisovin.magicspells.util.TargetInfo; +import com.nisovin.magicspells.util.MagicConfig; +import com.nisovin.magicspells.spells.TargetedSpell; +import com.nisovin.magicspells.util.config.ConfigData; +import com.nisovin.magicspells.util.glow.GlowManager; +import com.nisovin.magicspells.spells.TargetedEntitySpell; +import com.nisovin.magicspells.util.glow.impl.PacketEventsGlowManager; + +public class GlowSpell extends TargetedSpell implements TargetedEntitySpell { + + private static GlowManager glowManager; + + private final ConfigData global; + private final ConfigData remove; + private final ConfigData duration; + private final ConfigData priority; + private final ConfigData key; + private final ConfigData color; + + public GlowSpell(MagicConfig config, String spellName) { + super(config, spellName); + + key = getConfigDataNamespacedKey("key", null); + color = getConfigDataNamedTextColor("color", null); + global = getConfigDataBoolean("global", true); + remove = getConfigDataBoolean("remove", false); + duration = getConfigDataInt("duration", 0); + priority = getConfigDataInt("priority", 0); + + if (glowManager == null) { + if (Bukkit.getPluginManager().isPluginEnabled("packetevents")) glowManager = new PacketEventsGlowManager(); + else glowManager = MagicSpells.getVolatileCodeHandler().getGlowManager(); + + glowManager.load(); + } + } + + @Override + public CastResult cast(SpellData data) { + TargetInfo info = getTargetedEntity(data); + if (info.noTarget()) return noTarget(info); + + return castAtEntity(info.spellData()); + } + + @Override + public CastResult castAtEntity(SpellData data) { + if (global.get(data)) { + NamespacedKey key = this.key.get(data); + + if (remove.get(data)) { + if (key == null) return new CastResult(PostCastAction.ALREADY_HANDLED, data); + + glowManager.removeGlow(data.target(), key); + } else { + glowManager.applyGlow( + data.target(), + key != null ? key : new NamespacedKey(MagicSpells.getInstance(), UUID.randomUUID().toString()), + color.get(data), + priority.get(data), + duration.get(data) + ); + } + + playSpellEffects(data); + return new CastResult(PostCastAction.HANDLE_NORMALLY, data); + } + + if (!(data.caster() instanceof Player caster)) return new CastResult(PostCastAction.ALREADY_HANDLED, data); + + NamespacedKey key = this.key.get(data); + + if (remove.get(data)) { + if (key == null) return new CastResult(PostCastAction.ALREADY_HANDLED, data); + + glowManager.removeGlow(caster, data.target(), key); + } else { + glowManager.applyGlow( + caster, + data.target(), + key != null ? key : new NamespacedKey(MagicSpells.getInstance(), UUID.randomUUID().toString()), + color.get(data), + priority.get(data), + duration.get(data) + ); + } + + playSpellEffects(data); + return new CastResult(PostCastAction.HANDLE_NORMALLY, data); + } + + @Override + protected void turnOff() { + if (glowManager == null) return; + + glowManager.unload(); + glowManager = null; + } + +} diff --git a/core/src/main/java/com/nisovin/magicspells/spells/targeted/ext/GlowSpell.java b/core/src/main/java/com/nisovin/magicspells/spells/targeted/ext/GlowSpell.java index 145cf04cc..9c586fe4a 100644 --- a/core/src/main/java/com/nisovin/magicspells/spells/targeted/ext/GlowSpell.java +++ b/core/src/main/java/com/nisovin/magicspells/spells/targeted/ext/GlowSpell.java @@ -25,6 +25,11 @@ @DependsOn({"ProtocolLib", "XGlow"}) public class GlowSpell extends TargetedSpell implements TargetedEntitySpell { + private static final DeprecationNotice DEPRECATION_NOTICE = new DeprecationNotice( + "The '.targeted.ext.GlowSpell' spell class does not function, as the XGlow plugin is abandoned.", + "Use the '.targeted.GlowSpell' spell class." + ); + private final Multimap glowing; private final ConfigData color; @@ -43,6 +48,8 @@ public GlowSpell(MagicConfig config, String spellName) { powerAffectsDuration = getConfigDataBoolean("power-affects-duration", true); color = getConfigDataEnum("color", ChatColor.class, ChatColor.WHITE); + + MagicSpells.getDeprecationManager().addDeprecation(this, DEPRECATION_NOTICE); } @Override diff --git a/core/src/main/java/com/nisovin/magicspells/util/config/ConfigDataUtil.java b/core/src/main/java/com/nisovin/magicspells/util/config/ConfigDataUtil.java index fe32cb6ee..6f59ba0b2 100644 --- a/core/src/main/java/com/nisovin/magicspells/util/config/ConfigDataUtil.java +++ b/core/src/main/java/com/nisovin/magicspells/util/config/ConfigDataUtil.java @@ -7,6 +7,7 @@ import org.jetbrains.annotations.Nullable; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.*; import org.bukkit.util.Vector; @@ -19,6 +20,7 @@ import org.bukkit.configuration.ConfigurationSection; import com.nisovin.magicspells.util.*; +import com.nisovin.magicspells.MagicSpells; import com.nisovin.magicspells.handlers.PotionEffectHandler; public class ConfigDataUtil { @@ -758,6 +760,44 @@ public boolean isConstant() { }; } + public static ConfigData getNamedTextColor(@NotNull ConfigurationSection config, @NotNull String path, @Nullable NamedTextColor def) { + String value = config.getString(path); + if (value == null) return data -> def; + + NamedTextColor val = NamedTextColor.NAMES.value(value.toLowerCase()); + if (val != null) return data -> val; + + ConfigData supplier = getString(value); + if (supplier.isConstant()) return data -> def; + + return (VariableConfigData) data -> { + String string = supplier.get(data); + if (string == null) return def; + + NamedTextColor color = NamedTextColor.NAMES.value(string.toLowerCase()); + return color == null ? def : color; + }; + } + + public static ConfigData getNamespacedKey(@NotNull ConfigurationSection config, @NotNull String path, @Nullable NamespacedKey def) { + String value = config.getString(path); + if (value == null) return data -> def; + + NamespacedKey val = NamespacedKey.fromString(value.toLowerCase()); + if (val != null) return data -> val; + + ConfigData supplier = getString(value); + if (supplier.isConstant()) return data -> def; + + return (VariableConfigData) data -> { + String string = supplier.get(data); + if (string == null) return def; + + NamespacedKey key = NamespacedKey.fromString(string.toLowerCase()); + return key == null ? def : key; + }; + } + public static ConfigData getAngle(@NotNull ConfigurationSection config, @NotNull String path, @Nullable Angle def) { if (config.isInt(path) || config.isLong(path) || config.isDouble(path)) { float value = (float) config.getDouble(path); diff --git a/core/src/main/java/com/nisovin/magicspells/util/glow/impl/PacketEventsGlowManager.java b/core/src/main/java/com/nisovin/magicspells/util/glow/impl/PacketEventsGlowManager.java new file mode 100644 index 000000000..a6fd4ad7b --- /dev/null +++ b/core/src/main/java/com/nisovin/magicspells/util/glow/impl/PacketEventsGlowManager.java @@ -0,0 +1,241 @@ +package com.nisovin.magicspells.util.glow.impl; + +import org.jetbrains.annotations.NotNull; + +import java.util.*; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.event.Listener; +import org.bukkit.scoreboard.Team; +import org.bukkit.scoreboard.Scoreboard; +import org.bukkit.configuration.ConfigurationSection; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.event.PacketSendEvent; +import com.github.retrooper.packetevents.wrapper.PacketWrapper; +import com.github.retrooper.packetevents.event.PacketListenerAbstract; +import com.github.retrooper.packetevents.event.PacketListenerPriority; +import com.github.retrooper.packetevents.manager.player.PlayerManager; +import com.github.retrooper.packetevents.protocol.packettype.PacketType; +import com.github.retrooper.packetevents.protocol.entity.data.EntityData; +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerTeams; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerTeams.*; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityMetadata; + +import com.nisovin.magicspells.MagicSpells; +import com.nisovin.magicspells.util.glow.LibsDisguiseHelper; +import com.nisovin.magicspells.util.glow.PacketBasedGlowManager; + +public class PacketEventsGlowManager extends PacketBasedGlowManager, WrapperPlayServerEntityMetadata, WrapperPlayServerTeams> { + + private final GlowPacketListener listener; + + public PacketEventsGlowManager() { + listener = new GlowPacketListener(); + + MagicSpells.registerEvents(this); + } + + @Override + public void load() { + super.load(); + + PacketEvents.getAPI().getEventManager().registerListener(listener); + } + + @Override + public synchronized void unload() { + PacketEvents.getAPI().getEventManager().unregisterListener(listener); + + super.unload(); + } + + @Override + protected Collection createAddTeamPackets() { + ConfigurationSection config = MagicSpells.getInstance().getMagicConfig().getMainConfig(); + + boolean seeFriendlyInvisibles = config.getBoolean("general.glow-spell-scoreboard-teams.see-friendly-invisibles", false); + OptionData optionData = seeFriendlyInvisibles ? OptionData.ALL : OptionData.FRIENDLY_FIRE; + + NameTagVisibility visibility = getStringOption("name-tag-visibility", NameTagVisibility.ALWAYS, NameTagVisibility::fromID, config, MagicSpells::error); + CollisionRule collisionRule = getStringOption("collision-rule", CollisionRule.ALWAYS, CollisionRule::fromID, config, MagicSpells::error); + + return NamedTextColor.NAMES.values() + .stream() + .map(color -> { + String name = "magicspells:" + color; + + return new WrapperPlayServerTeams( + name, + TeamMode.CREATE, + new WrapperPlayServerTeams.ScoreBoardTeamInfo( + Component.text(name), + null, + null, + visibility, + collisionRule, + color, + optionData + ) + ); + }) + .toList(); + } + + @Override + protected Collection createRemoveTeamPackets() { + return NamedTextColor.NAMES.values() + .stream() + .map(color -> { + String name = "magicspells:" + color; + return new WrapperPlayServerTeams(name, TeamMode.REMOVE, (WrapperPlayServerTeams.ScoreBoardTeamInfo) null); + }) + .toList(); + } + + @Override + protected WrapperPlayServerEntityMetadata createEntityDataPacket(@NotNull Entity entity, boolean forceGlow) { + byte metadata = MagicSpells.getVolatileCodeHandler().getEntityMetadata(entity); + if (forceGlow) metadata |= 0x40; + + return new WrapperPlayServerEntityMetadata( + entity.getEntityId(), + List.of(new EntityData(0, EntityDataTypes.BYTE, metadata)) + ); + } + + @Override + protected Collection createJoinTeamPacket(@NotNull GlowData data) { + return data.lastScoreboardEntry().map(entry -> new WrapperPlayServerTeams( + "magicspells:" + data.color(), + TeamMode.ADD_ENTITIES, + (WrapperPlayServerTeams.ScoreBoardTeamInfo) null, + entry + )); + } + + @Override + protected Collection createResetTeamPacket(@NotNull Scoreboard scoreboard, @NotNull GlowData data) { + return data.lastScoreboardEntry().map(entry -> { + Team team = scoreboard.getEntryTeam(entry); + if (team != null) { + return new WrapperPlayServerTeams( + team.getName(), + TeamMode.ADD_ENTITIES, + (WrapperPlayServerTeams.ScoreBoardTeamInfo) null, + entry + ); + } + + return new WrapperPlayServerTeams( + "magicspells:" + data.color(), + TeamMode.REMOVE_ENTITIES, + (WrapperPlayServerTeams.ScoreBoardTeamInfo) null, + entry + ); + }); + } + + @Override + protected void sendPacket(@NotNull Player player, @NotNull PacketWrapper packetWrapper) { + PlayerManager manager = PacketEvents.getAPI().getPlayerManager(); + if (packetWrapper instanceof WrapperPlayServerEntityMetadata) manager.sendPacket(player, packetWrapper); + else manager.sendPacketSilently(player, packetWrapper); + } + + @Override + protected void registerEvents(Listener listener) { + MagicSpells.registerEvents(listener); + } + + @Override + protected void cancelTask(int taskId) { + MagicSpells.cancelTask(taskId); + } + + @Override + public int scheduleDelayedTask(Runnable runnable, long delay) { + return MagicSpells.scheduleDelayedTask(runnable, delay); + } + + private final class GlowPacketListener extends PacketListenerAbstract { + + public GlowPacketListener() { + super(PacketListenerPriority.LOWEST); + } + + @Override + public void onPacketSend(PacketSendEvent event) { + synchronized (PacketEventsGlowManager.this) { + if (glows.isEmpty() && perPlayerGlows.isEmpty()) + return; + } + + try { + if (event.getPacketType() == PacketType.Play.Server.ENTITY_METADATA) handleEntityData(event); + else if (event.getPacketType() == PacketType.Play.Server.TEAMS) handleTeams(event); + } catch (Throwable throwable) { + MagicSpells.error("Encountered an error while intercepting a packet - cancelling packet send."); + throwable.printStackTrace(); + + event.setCancelled(true); + } + } + + private void handleEntityData(PacketSendEvent event) { + WrapperPlayServerEntityMetadata packet = new WrapperPlayServerEntityMetadata(event); + + List metadata = packet.getEntityMetadata(); + if (metadata.isEmpty()) return; + + EntityData entityData = metadata.getFirst(); + if (entityData.getIndex() != 0) return; + + byte flags = (byte) entityData.getValue(); + if ((flags & 0x40) > 0) return; + + Player player = event.getPlayer(); + + UUID uuid; + if (!libsDisguisesLoaded || packet.getEntityId() != LibsDisguiseHelper.getSelfDisguiseId()) { + Entity entity = MagicSpells.getVolatileCodeHandler().getEntityFromId(player.getWorld(), packet.getEntityId()); + if (entity == null) return; + + uuid = entity.getUniqueId(); + } else uuid = player.getUniqueId(); + + GlowData data = getGlowData(player.getUniqueId(), uuid); + if (data == null) return; + + flags |= 0x40; + entityData.setValue(flags); + } + + private void handleTeams(PacketSendEvent event) { + WrapperPlayServerTeams packet = new WrapperPlayServerTeams(event); + + TeamMode mode = packet.getTeamMode(); + if (mode != TeamMode.REMOVE_ENTITIES && mode != TeamMode.ADD_ENTITIES) return; + + Collection entries = packet.getPlayers(); + if (entries.isEmpty()) return; + + Collection filtered = filterTeamEntries(event.getPlayer(), entries); + if (filtered == null) return; + + if (filtered.isEmpty()) { + event.setCancelled(true); + return; + } + + packet.setPlayers(filtered); + } + + } + +} diff --git a/core/src/main/java/com/nisovin/magicspells/volatilecode/ManagerVolatile.java b/core/src/main/java/com/nisovin/magicspells/volatilecode/ManagerVolatile.java index 3ace4ed5e..424349364 100644 --- a/core/src/main/java/com/nisovin/magicspells/volatilecode/ManagerVolatile.java +++ b/core/src/main/java/com/nisovin/magicspells/volatilecode/ManagerVolatile.java @@ -3,6 +3,8 @@ import java.util.Map; import org.bukkit.Bukkit; +import org.bukkit.event.Listener; +import org.bukkit.configuration.file.YamlConfiguration; import com.nisovin.magicspells.MagicSpells; import com.nisovin.magicspells.volatilecode.latest.VolatileCodeLatest; @@ -24,6 +26,21 @@ public int scheduleDelayedTask(Runnable task, long delay) { return MagicSpells.scheduleDelayedTask(task, delay); } + @Override + public void cancelTask(int id) { + MagicSpells.cancelTask(id); + } + + @Override + public void registerEvents(Listener listener) { + MagicSpells.registerEvents(listener); + } + + @Override + public YamlConfiguration getMainConfig() { + return MagicSpells.getInstance().getMagicConfig().getMainConfig(); + } + }; public static VolatileCodeHandle constructVolatileCodeHandler() { diff --git a/core/src/main/java/com/nisovin/magicspells/volatilecode/VolatileCodeDisabled.java b/core/src/main/java/com/nisovin/magicspells/volatilecode/VolatileCodeDisabled.java index 499c5476e..b81f74142 100644 --- a/core/src/main/java/com/nisovin/magicspells/volatilecode/VolatileCodeDisabled.java +++ b/core/src/main/java/com/nisovin/magicspells/volatilecode/VolatileCodeDisabled.java @@ -1,6 +1,7 @@ package com.nisovin.magicspells.volatilecode; import org.bukkit.Sound; +import org.bukkit.World; import org.bukkit.entity.*; import org.bukkit.Location; import org.bukkit.util.Vector; @@ -10,6 +11,8 @@ import io.papermc.paper.advancement.AdvancementDisplay.Frame; +import com.nisovin.magicspells.util.glow.GlowManager; + public class VolatileCodeDisabled extends VolatileCodeHandle { public VolatileCodeDisabled() { @@ -63,4 +66,19 @@ public void clearGameTestMarkers(Player player) { } + @Override + public byte getEntityMetadata(Entity entity) { + return 0; + } + + @Override + public Entity getEntityFromId(World world, int id) { + return null; + } + + @Override + public GlowManager getGlowManager() { + return null; + } + } diff --git a/core/src/main/resources/general.yml b/core/src/main/resources/general.yml index 617c99f96..159571020 100644 --- a/core/src/main/resources/general.yml +++ b/core/src/main/resources/general.yml @@ -152,3 +152,7 @@ entity-names: mooshroom: a deformed cow squid: a slimy squid iron_golem: a friendly iron golem +glow-spell-scoreboard-teams: + see-friendly-invisibles: false + name-tag-visibility: always + collision-rule: always diff --git a/core/src/main/resources/plugin.yml b/core/src/main/resources/plugin.yml index 81d1f807e..d37180242 100644 --- a/core/src/main/resources/plugin.yml +++ b/core/src/main/resources/plugin.yml @@ -13,3 +13,4 @@ softdepend: - NoCheatPlus - PlaceholderAPI - XGlow + - packetevents diff --git a/nms/latest/src/main/java/com/nisovin/magicspells/volatilecode/latest/VolatileCodeLatest.java b/nms/latest/src/main/java/com/nisovin/magicspells/volatilecode/latest/VolatileCodeLatest.java index ace6e5f27..6648860c7 100644 --- a/nms/latest/src/main/java/com/nisovin/magicspells/volatilecode/latest/VolatileCodeLatest.java +++ b/nms/latest/src/main/java/com/nisovin/magicspells/volatilecode/latest/VolatileCodeLatest.java @@ -4,9 +4,11 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; +import org.bukkit.World; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.util.Vector; +import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import org.bukkit.entity.LivingEntity; import org.bukkit.inventory.ItemStack; @@ -14,6 +16,7 @@ import org.bukkit.craftbukkit.CraftWorld; import org.bukkit.craftbukkit.CraftServer; +import org.bukkit.craftbukkit.entity.CraftEntity; import org.bukkit.craftbukkit.entity.CraftPlayer; import org.bukkit.craftbukkit.entity.CraftTNTPrimed; import org.bukkit.craftbukkit.inventory.CraftItemStack; @@ -25,6 +28,7 @@ import io.papermc.paper.adventure.PaperAdventure; import io.papermc.paper.advancement.AdvancementDisplay; +import com.nisovin.magicspells.util.glow.GlowManager; import com.nisovin.magicspells.volatilecode.VolatileCodeHandle; import com.nisovin.magicspells.volatilecode.VolatileCodeHelper; @@ -55,12 +59,17 @@ public class VolatileCodeLatest extends VolatileCodeHandle { private final EntityDataAccessor> DATA_EFFECT_PARTICLES; private final EntityDataAccessor DATA_EFFECT_AMBIENCE_ID; + private final EntityDataAccessor DATA_SHARED_FLAGS_ID; private final Method UPDATE_EFFECT_PARTICLES; @SuppressWarnings("unchecked") public VolatileCodeLatest(VolatileCodeHelper helper) throws Exception { super(helper); + Field dataSharedFlagsIdField = net.minecraft.world.entity.Entity.class.getDeclaredField("DATA_SHARED_FLAGS_ID"); + dataSharedFlagsIdField.setAccessible(true); + DATA_SHARED_FLAGS_ID = (EntityDataAccessor) dataSharedFlagsIdField.get(null); + Class nmsEntityClass = net.minecraft.world.entity.LivingEntity.class; Field dataEffectParticlesField = nmsEntityClass.getDeclaredField("DATA_EFFECT_PARTICLES"); @@ -211,4 +220,20 @@ public void clearGameTestMarkers(Player player) { ((CraftPlayer) player).getHandle().connection.send(new ClientboundCustomPayloadPacket(payload)); } + @Override + public byte getEntityMetadata(Entity entity) { + return ((CraftEntity) entity).getHandle().getEntityData().get(DATA_SHARED_FLAGS_ID); + } + + @Override + public Entity getEntityFromId(World world, int id) { + var entity = ((CraftWorld) world).getHandle().moonrise$getEntityLookup().get(id); + return entity == null ? null : entity.getBukkitEntity(); + } + + @Override + public GlowManager getGlowManager() { + return new VolatileGlowManagerLatest(helper); + } + } diff --git a/nms/latest/src/main/java/com/nisovin/magicspells/volatilecode/latest/VolatileGlowManagerLatest.java b/nms/latest/src/main/java/com/nisovin/magicspells/volatilecode/latest/VolatileGlowManagerLatest.java new file mode 100644 index 000000000..2bd839483 --- /dev/null +++ b/nms/latest/src/main/java/com/nisovin/magicspells/volatilecode/latest/VolatileGlowManagerLatest.java @@ -0,0 +1,320 @@ +package com.nisovin.magicspells.volatilecode.latest; + +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.lang.invoke.MethodType; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; + +import io.netty.channel.ChannelPromise; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandlerAdapter; + +import net.minecraft.ChatFormatting; +import net.minecraft.network.protocol.Packet; +import net.minecraft.world.scores.PlayerTeam; +import net.minecraft.world.scores.Scoreboard; +import net.minecraft.world.scores.Team.Visibility; +import net.minecraft.world.scores.Team.CollisionRule; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket; +import net.minecraft.network.protocol.game.ClientboundSetPlayerTeamPacket; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.event.Listener; +import org.bukkit.scoreboard.Team; +import org.bukkit.event.EventHandler; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.craftbukkit.entity.CraftEntity; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.bukkit.configuration.ConfigurationSection; + +import com.nisovin.magicspells.util.glow.LibsDisguiseHelper; +import com.nisovin.magicspells.volatilecode.VolatileCodeHelper; +import com.nisovin.magicspells.util.glow.PacketBasedGlowManager; + +public class VolatileGlowManagerLatest extends PacketBasedGlowManager, ClientboundSetEntityDataPacket, ClientboundSetPlayerTeamPacket> { + + private static final EntityDataAccessor DATA_SHARED_FLAGS_ID = new EntityDataAccessor<>(0, EntityDataSerializers.BYTE); + + private final Set> handled = Collections.synchronizedSet(Collections.newSetFromMap(new WeakHashMap<>())); + private final MethodHandle teamPacketHandle; + private final VolatileCodeHelper helper; + + public VolatileGlowManagerLatest(VolatileCodeHelper helper) { + this.helper = helper; + + try { + teamPacketHandle = MethodHandles + .privateLookupIn(ClientboundSetPlayerTeamPacket.class, MethodHandles.lookup()) + .findConstructor( + ClientboundSetPlayerTeamPacket.class, + MethodType.methodType(void.class, String.class, int.class, Optional.class, Collection.class) + ); + } catch (Exception e) { + throw new RuntimeException("Encountered an error while initializing VolatileGlowManagerLatest", e); + } + + helper.registerEvents(this); + } + + @Override + public void load() { + super.load(); + + Bukkit.getOnlinePlayers().forEach(this::addGlowChannelHandler); + } + + @Override + public synchronized void unload() { + Bukkit.getOnlinePlayers().forEach(this::removeGlowChannelHandler); + + super.unload(); + handled.clear(); + } + + @Override + protected Collection createAddTeamPackets() { + List teamPackets = new ArrayList<>(); + + ConfigurationSection config = helper.getMainConfig(); + boolean seeFriendlyInvisibles = config.getBoolean("general.glow-spell-scoreboard-teams.see-friendly-invisibles", false); + CollisionRule collision = getStringOption("collision-rule", CollisionRule.ALWAYS, CollisionRule::byName, config, helper::error); + Visibility visibility = getStringOption("name-tag-visibility", Visibility.ALWAYS, Visibility::byName, config, helper::error); + + Scoreboard scoreboard = new Scoreboard(); + for (ChatFormatting formatting : ChatFormatting.values()) { + if (!formatting.isColor()) continue; + + PlayerTeam team = new PlayerTeam(scoreboard, "magicspells:" + formatting.getName()); + team.setSeeFriendlyInvisibles(seeFriendlyInvisibles); + team.setNameTagVisibility(visibility); + team.setCollisionRule(collision); + team.setColor(formatting); + + teamPackets.add(ClientboundSetPlayerTeamPacket.createAddOrModifyPacket(team, true)); + } + + return teamPackets; + } + + @Override + protected Collection createRemoveTeamPackets() { + List packets = new ArrayList<>(); + + Scoreboard scoreboard = new Scoreboard(); + for (ChatFormatting formatting : ChatFormatting.values()) { + if (!formatting.isColor()) continue; + + PlayerTeam team = new PlayerTeam(scoreboard, "magicspells:" + formatting.getName()); + packets.add(ClientboundSetPlayerTeamPacket.createRemovePacket(team)); + } + + return packets; + } + + @Override + protected ClientboundSetEntityDataPacket createEntityDataPacket(@NotNull Entity entity, boolean forceGlow) { + byte metadata = ((CraftEntity) entity).getHandle().getEntityData().get(DATA_SHARED_FLAGS_ID); + if (forceGlow) metadata |= 0x40; + + return new ClientboundSetEntityDataPacket( + entity.getEntityId(), + list(new SynchedEntityData.DataValue<>( + 0, + EntityDataSerializers.BYTE, + metadata + )) + ); + } + + @Override + protected Collection createJoinTeamPacket(@NotNull GlowData data) { + return data.lastScoreboardEntry().map(entry -> { + try { + return (ClientboundSetPlayerTeamPacket) teamPacketHandle.invoke( + "magicspells:" + data.color(), + 3, + Optional.empty(), + list(entry) + ); + } catch (Throwable e) { + throw new RuntimeException(e); + } + }); + } + + @Override + protected Collection createResetTeamPacket(org.bukkit.scoreboard.@NotNull Scoreboard scoreboard, @NotNull GlowData data) { + return data.lastScoreboardEntry().map(entry -> { + try { + Team team = scoreboard.getEntryTeam(entry); + if (team != null) { + return (ClientboundSetPlayerTeamPacket) teamPacketHandle.invoke( + team.getName(), + 3, + Optional.empty(), + list(entry) + ); + } + + return (ClientboundSetPlayerTeamPacket) teamPacketHandle.invoke( + "magicspells:" + data.color(), + 4, + Optional.empty(), + list(entry) + ); + } catch (Throwable e) { + throw new RuntimeException(e); + } + }); + } + + @Override + protected void sendPacket(@NotNull Player player, @NotNull Packet packet) { + if (!(packet instanceof ClientboundSetPlayerTeamPacket teamPacket) || teamPacket.getTeamAction() == null) + handled.add(packet); + + ServerGamePacketListenerImpl connection = ((CraftPlayer) player).getHandle().connection; + connection.send(packet); + } + + @Override + protected void registerEvents(Listener listener) { + helper.registerEvents(listener); + } + + @Override + protected void cancelTask(int taskId) { + helper.cancelTask(taskId); + } + + @Override + public int scheduleDelayedTask(Runnable runnable, long delay) { + return helper.scheduleDelayedTask(runnable, delay); + } + + private void addGlowChannelHandler(Player player) { + ChannelPipeline pipeline = ((CraftPlayer) player).getHandle().connection.connection.channel.pipeline(); + pipeline.addBefore("unbundler", "magicspells:glow_channel_handler", new GlowChannelHandler(player)); + } + + private void removeGlowChannelHandler(Player player) { + ChannelPipeline pipeline = ((CraftPlayer) player).getHandle().connection.connection.channel.pipeline(); + + if (pipeline.get("magicspells:glow_channel_handler") != null) + pipeline.remove("magicspells:glow_channel_handler"); + } + + private List list(T element) { + List list = new ArrayList<>(1); + list.add(element); + + return list; + } + + @Override + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + super.onPlayerJoin(event); + addGlowChannelHandler(event.getPlayer()); + } + + @EventHandler + public void onPlayerQuit(PlayerQuitEvent event) { + removeGlowChannelHandler(event.getPlayer()); + } + + private class GlowChannelHandler extends ChannelOutboundHandlerAdapter { + + private final Player player; + + private GlowChannelHandler(Player player) { + this.player = player; + } + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + if (msg instanceof Packet p && handled.contains(p)) { + super.write(ctx, msg, promise); + return; + } + + synchronized (VolatileGlowManagerLatest.this) { + if (glows.isEmpty() && perPlayerGlows.isEmpty()) { + super.write(ctx, msg, promise); + return; + } + } + + switch (msg) { + case ClientboundSetEntityDataPacket packet -> handleEntityData(packet); + case ClientboundSetPlayerTeamPacket packet -> { + msg = handleTeamPacket(packet); + if (msg == null) return; + } + default -> {} + } + + super.write(ctx, msg, promise); + } + + private void handleEntityData(ClientboundSetEntityDataPacket packet) { + List> packedItems = packet.packedItems(); + if (packedItems.isEmpty()) return; + + SynchedEntityData.DataValue item = packedItems.getFirst(); + if (item.id() != 0) return; + + byte flags = (byte) item.value(); + if ((flags & 0x40) > 0) return; + + UUID uuid; + if (!libsDisguisesLoaded || packet.id() != LibsDisguiseHelper.getSelfDisguiseId()) { + var entity = ((CraftPlayer) player).getHandle().level().moonrise$getEntityLookup().get(packet.id()); + if (entity == null) return; + + uuid = entity.getUUID(); + } else uuid = player.getUniqueId(); + + GlowData data = getGlowData(player.getUniqueId(), uuid); + if (data == null) return; + + flags |= 0x40; + packedItems.set(0, new SynchedEntityData.DataValue<>(0, EntityDataSerializers.BYTE, flags)); + } + + private ClientboundSetPlayerTeamPacket handleTeamPacket(ClientboundSetPlayerTeamPacket packet) { + ClientboundSetPlayerTeamPacket.Action playerAction = packet.getPlayerAction(); + if (playerAction == null || packet.getTeamAction() != null) return packet; + + Collection entries = packet.getPlayers(); + if (entries.isEmpty()) return packet; + + Collection filtered = filterTeamEntries(player, entries); + if (filtered == null) return packet; + if (filtered.isEmpty()) return null; + + try { + return (ClientboundSetPlayerTeamPacket) teamPacketHandle.invoke( + packet.getName(), + playerAction == ClientboundSetPlayerTeamPacket.Action.ADD ? 3 : 4, + packet.getParameters(), + filtered + ); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + } + +} diff --git a/nms/shared/build.gradle b/nms/shared/build.gradle index e69de29bb..6eeba53ee 100644 --- a/nms/shared/build.gradle +++ b/nms/shared/build.gradle @@ -0,0 +1,3 @@ +dependencies { + implementation(group: "me.libraryaddict.disguises", name: "libsdisguises", version: "10.0.44-SNAPSHOT") { transitive = false } +} diff --git a/nms/shared/src/main/java/com/nisovin/magicspells/util/glow/GlowManager.java b/nms/shared/src/main/java/com/nisovin/magicspells/util/glow/GlowManager.java new file mode 100644 index 000000000..10678fa98 --- /dev/null +++ b/nms/shared/src/main/java/com/nisovin/magicspells/util/glow/GlowManager.java @@ -0,0 +1,26 @@ +package com.nisovin.magicspells.util.glow; + +import org.jetbrains.annotations.Range; +import org.jetbrains.annotations.NotNull; + +import net.kyori.adventure.text.format.NamedTextColor; + +import org.bukkit.NamespacedKey; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; + +public interface GlowManager { + + void load(); + + void unload(); + + void applyGlow(@NotNull Entity entity, @NotNull NamespacedKey key, @NotNull NamedTextColor color, int priority, @Range(from = 0, to = Integer.MAX_VALUE) int duration); + + void applyGlow(@NotNull Player player, @NotNull Entity entity, @NotNull NamespacedKey key, @NotNull NamedTextColor color, int priority, @Range(from = 0, to = Integer.MAX_VALUE) int duration); + + void removeGlow(@NotNull Entity entity, @NotNull NamespacedKey key); + + void removeGlow(@NotNull Player player, @NotNull Entity entity, @NotNull NamespacedKey key); + +} diff --git a/nms/shared/src/main/java/com/nisovin/magicspells/util/glow/LibsDisguiseHelper.java b/nms/shared/src/main/java/com/nisovin/magicspells/util/glow/LibsDisguiseHelper.java new file mode 100644 index 000000000..1e64fb70f --- /dev/null +++ b/nms/shared/src/main/java/com/nisovin/magicspells/util/glow/LibsDisguiseHelper.java @@ -0,0 +1,70 @@ +package com.nisovin.magicspells.util.glow; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.event.Listener; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; + +import me.libraryaddict.disguise.DisguiseAPI; +import me.libraryaddict.disguise.events.DisguiseEvent; +import me.libraryaddict.disguise.disguisetypes.Disguise; +import me.libraryaddict.disguise.events.UndisguiseEvent; +import me.libraryaddict.disguise.disguisetypes.PlayerDisguise; + +import com.nisovin.magicspells.util.glow.PacketBasedGlowManager.ScoreboardEntry; + +public class LibsDisguiseHelper { + + public static ScoreboardEntry getDisguisedScoreboardEntry(@Nullable Player player, @NotNull Entity entity) { + Disguise disguise = player == null ? DisguiseAPI.getDisguise(entity) : DisguiseAPI.getDisguise(player, entity); + if (disguise == null) return new ScoreboardEntry(entity.getScoreboardEntryName()); + + if (entity.equals(player) && !disguise.isSelfDisguiseVisible()) + return new ScoreboardEntry(entity.getScoreboardEntryName()); + + return getDisguisedScoreboardEntry(entity, disguise); + } + + public static ScoreboardEntry getDisguisedScoreboardEntry(@NotNull Entity entity, @NotNull Disguise disguise) { + String entry = disguise instanceof PlayerDisguise pd ? pd.getProfileName() : disguise.getUUID().toString(); + return new ScoreboardEntry(entity.getScoreboardEntryName(), entry); + } + + public static int getSelfDisguiseId() { + return DisguiseAPI.getSelfDisguiseId(); + } + + public static Listener createLibDisguisesListener(PacketBasedGlowManager glowManager) { + return new Listener() { + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onDisguise(DisguiseEvent event) { + Disguise disguise = event.getDisguise(); + if (!disguise.isSelfDisguiseVisible() || !(event.getDisguised() instanceof Player player)) + return; + + // Self-disguising is usually delayed by 2 ticks, at the time of writing. As such, we must delay refreshing + // the scoreboard entry to avoid glow colors flickering. Adjust as necessary for disguise delay. + glowManager.scheduleDelayedTask(() -> { + if (disguise.isDisguiseInUse()) + glowManager.refreshScoreboardEntry(player, getDisguisedScoreboardEntry(player, disguise)); + }, 2); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onUndisguise(UndisguiseEvent event) { + Disguise disguise = event.getDisguise(); + if (!disguise.isSelfDisguiseVisible() || !(event.getDisguised() instanceof Player player)) + return; + + glowManager.refreshScoreboardEntry(player, new ScoreboardEntry(player.getScoreboardEntryName())); + } + + }; + } + +} diff --git a/nms/shared/src/main/java/com/nisovin/magicspells/util/glow/PacketBasedGlowManager.java b/nms/shared/src/main/java/com/nisovin/magicspells/util/glow/PacketBasedGlowManager.java new file mode 100644 index 000000000..04c34094a --- /dev/null +++ b/nms/shared/src/main/java/com/nisovin/magicspells/util/glow/PacketBasedGlowManager.java @@ -0,0 +1,587 @@ +package com.nisovin.magicspells.util.glow; + +import org.jetbrains.annotations.Range; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.concurrent.ConcurrentHashMap; + +import it.unimi.dsi.fastutil.Pair; +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; +import it.unimi.dsi.fastutil.objects.ObjectObjectMutablePair; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; + +import com.google.common.collect.Iterables; + +import net.kyori.adventure.text.format.NamedTextColor; + +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.event.Listener; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.scoreboard.Scoreboard; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.configuration.ConfigurationSection; + +import io.papermc.paper.event.player.PlayerTrackEntityEvent; +import io.papermc.paper.event.player.PlayerUntrackEntityEvent; + +public abstract class PacketBasedGlowManager implements GlowManager, Listener { + + protected final Object2ObjectMap, GlowDataMap> perPlayerGlows = new Object2ObjectOpenHashMap<>(); + protected final Object2ObjectMap glows = new Object2ObjectOpenHashMap<>(); + protected final ConcurrentHashMap scoreboardNames = new ConcurrentHashMap<>(); + + protected Collection addTeamPackets; + protected boolean libsDisguisesLoaded = false; + protected int sequence = Integer.MIN_VALUE; + + @Override + public void load() { + addTeamPackets = createAddTeamPackets(); + Bukkit.getOnlinePlayers().forEach(player -> sendPackets(player, addTeamPackets)); + + if (Bukkit.getPluginManager().isPluginEnabled("LibsDisguises")) { + libsDisguisesLoaded = true; + registerEvents(LibsDisguiseHelper.createLibDisguisesListener(this)); + } + } + + @Override + public synchronized void unload() { + glows.forEach((key, value) -> { + Entity entity = Bukkit.getEntity(key); + if (entity == null) return; + + resetGlow(null, entity, value.get()); + }); + glows.clear(); + + perPlayerGlows.forEach((key, value) -> { + Player player = Bukkit.getPlayer(key.left()); + if (player == null) return; + + Entity entity = Bukkit.getEntity(key.right()); + if (entity == null) return; + + resetGlow(player, entity, value.get()); + }); + perPlayerGlows.clear(); + + scoreboardNames.clear(); + + Collection removeTeamPackets = createRemoveTeamPackets(); + Bukkit.getOnlinePlayers().forEach(player -> sendPackets(player, removeTeamPackets)); + } + + @Override + public void applyGlow(@NotNull Entity entity, @NotNull NamespacedKey key, @Nullable NamedTextColor color, int priority, @Range(from = 0, to = Integer.MAX_VALUE) int duration) { + GlowData data = new GlowData(priority, sequence++, color); + UUID uuid = entity.getUniqueId(); + + GlowData prev, curr; + synchronized (this) { + prev = getGlowData(null, uuid); + + GlowDataMap map = glows.computeIfAbsent(uuid, i -> new GlowDataMap()); + + GlowData old = map.put(key, data); + if (old != null && old.taskId != -1) cancelTask(old.taskId); + + if (duration > 0) { + data.taskId = scheduleDelayedTask(() -> { + synchronized (this) { + Entity e = Bukkit.getEntity(uuid); + if (e == null) { + map.remove(key); + if (map.isEmpty()) glows.remove(uuid); + return; + } + + removeGlow(e, key); + } + }, duration); + } + + curr = getGlowData(null, uuid); + } + + updateGlow(null, entity, prev, curr); + } + + @Override + public void applyGlow(@NotNull Player player, @NotNull Entity entity, @NotNull NamespacedKey key, @NotNull NamedTextColor color, int priority, @Range(from = 0, to = Integer.MAX_VALUE) int duration) { + Pair pair = Pair.of(player.getUniqueId(), entity.getUniqueId()); + GlowData data = new GlowData(priority, sequence++, color); + + GlowData prev, curr; + synchronized (this) { + prev = getGlowData(pair); + + GlowDataMap map = perPlayerGlows.computeIfAbsent(pair, i -> new GlowDataMap()); + + GlowData old = map.put(key, data); + if (old != null && old.taskId != -1) cancelTask(old.taskId); + + if (duration > 0) { + data.taskId = scheduleDelayedTask(() -> { + synchronized (this) { + Player p = Bukkit.getPlayer(pair.left()); + if (p == null) { + map.remove(key); + if (map.isEmpty()) perPlayerGlows.remove(pair); + return; + } + + Entity e = Bukkit.getEntity(pair.right()); + if (e == null) { + map.remove(key); + if (map.isEmpty()) perPlayerGlows.remove(pair); + return; + } + + removeGlow(p, e, key); + } + }, duration); + } + + curr = getGlowData(pair); + } + + updateGlow(player, entity, prev, curr); + } + + @Override + public void removeGlow(@NotNull Entity entity, @NotNull NamespacedKey key) { + UUID uuid = entity.getUniqueId(); + + GlowDataMap map = glows.get(uuid); + if (map == null) return; + + GlowData prev, curr; + synchronized (this) { + prev = getGlowData(null, uuid); + + map.remove(key); + if (map.isEmpty()) glows.remove(uuid); + + curr = getGlowData(null, uuid); + } + + if (curr == null) { + resetGlow(null, entity, prev); + return; + } + + updateGlow(null, entity, prev, curr); + } + + @Override + public void removeGlow(@NotNull Player player, @NotNull Entity entity, @NotNull NamespacedKey key) { + Pair pair = Pair.of(player.getUniqueId(), entity.getUniqueId()); + + GlowDataMap map = perPlayerGlows.get(pair); + if (map == null) return; + + GlowData prev, curr; + synchronized (this) { + prev = getGlowData(pair); + + map.remove(key); + if (map.isEmpty()) perPlayerGlows.remove(pair); + + curr = getGlowData(pair); + } + + if (curr == null) { + resetGlow(player, entity, prev); + return; + } + + updateGlow(player, entity, prev, curr); + } + + protected void updateGlow(@Nullable Player player, @NotNull Entity entity, @Nullable GlowData prev, @NotNull GlowData curr) { + if (prev == curr) return; + + Set trackedBy = entity.getTrackedBy(); + if (player != null && !player.equals(entity) && !trackedBy.contains(player)) return; + + if (prev != null && player != null && prev.color() == curr.color()) { + curr.lastScoreboardEntry(prev.lastScoreboardEntry); + return; + } + + ScoreboardEntry entry = getScoreboardEntry(player, entity); + + Scoreboard mainScoreboard = Bukkit.getScoreboardManager().getMainScoreboard(); + curr.lastScoreboardEntry(entry); + + TEntityDataPacket entityDataPacket = createEntityDataPacket(entity, true); + Collection teamPackets = Collections.emptyList(); + boolean reset = false; + if (curr.color != null) { + teamPackets = createJoinTeamPacket(curr); + entry.forEach(e -> scoreboardNames.put(e, entity.getUniqueId())); + } else if (prev != null) { + Scoreboard scoreboard = player == null ? mainScoreboard : player.getScoreboard(); + teamPackets = createResetTeamPacket(scoreboard, prev); + + entry.forEach(e -> scoreboardNames.put(e, entity.getUniqueId())); + reset = true; + } + + if (player != null) { + sendPackets(player, teamPackets); + sendPacket(player, entityDataPacket); + + return; + } + + Pair pair = ObjectObjectMutablePair.of(null, entity.getUniqueId()); + for (Player viewer : getTrackedBy(entity, trackedBy)) { + GlowDataMap targetedMap = perPlayerGlows.get(pair.left(viewer.getUniqueId())); + if (targetedMap != null) { + GlowData perPlayerData = targetedMap.get(); + if (perPlayerData.compareTo(curr) < 0) { + if (prev != null && perPlayerData.compareTo(prev) < 0) continue; + + updateGlow(viewer, entity, prev, perPlayerData); + continue; + } else if (prev != null && perPlayerData.compareTo(prev) < 0) { + updateGlow(viewer, entity, perPlayerData, curr); + continue; + } + } + + if (reset) { + Scoreboard scoreboard = viewer.getScoreboard(); + + if (scoreboard != mainScoreboard) { + sendPackets(viewer, createResetTeamPacket(scoreboard, prev)); + sendPacket(viewer, entityDataPacket); + continue; + } + } + + sendPackets(viewer, teamPackets); + sendPacket(viewer, entityDataPacket); + } + } + + protected void resetGlow(@Nullable Player player, @NotNull Entity entity, @NotNull GlowData data) { + Set trackedBy = entity.getTrackedBy(); + if (player != null && !player.equals(entity) && !trackedBy.contains(player)) return; + + Scoreboard mainScoreboard = Bukkit.getScoreboardManager().getMainScoreboard(); + + TEntityDataPacket entityDataPacket = createEntityDataPacket(entity, false); + Collection teamPackets = Collections.emptyList(); + if (data.color() != null) { + Scoreboard scoreboard = player == null ? mainScoreboard : player.getScoreboard(); + teamPackets = createResetTeamPacket(scoreboard, data); + } + + if (player != null) { + sendPackets(player, teamPackets); + sendPacket(player, entityDataPacket); + + return; + } + + Pair pair = ObjectObjectMutablePair.of(null, entity.getUniqueId()); + for (Player viewer : getTrackedBy(entity, trackedBy)) { + GlowDataMap targetedMap = perPlayerGlows.get(pair.left(viewer.getUniqueId())); + if (targetedMap != null) { + GlowData perPlayerData = targetedMap.get(); + if (perPlayerData.compareTo(data) < 0) continue; + + updateGlow(viewer, entity, data, perPlayerData); + continue; + } + + Scoreboard scoreboard = viewer.getScoreboard(); + if (scoreboard != mainScoreboard) { + sendPackets(viewer, createResetTeamPacket(scoreboard, data)); + sendPacket(viewer, entityDataPacket); + + continue; + } + + sendPackets(viewer, teamPackets); + sendPacket(viewer, entityDataPacket); + } + } + + void refreshScoreboardEntry(@NotNull Player player, @NotNull ScoreboardEntry entry) { + UUID uuid = player.getUniqueId(); + + GlowData data = getGlowData(uuid, uuid); + if (data == null || data.color() == null) return; + + Scoreboard scoreboard = player.getScoreboard(); + + Collection resetPackets = createResetTeamPacket(scoreboard, data); + sendPackets(player, resetPackets); + + data.lastScoreboardEntry(entry); + entry.map(e -> scoreboardNames.put(e, uuid)); + + Collection joinPackets = createJoinTeamPacket(data); + sendPackets(player, joinPackets); + } + + protected GlowData getGlowData(@Nullable UUID player, @NotNull UUID entity) { + synchronized (this) { + GlowDataMap map = glows.get(entity); + GlowData data = map == null ? null : map.get(); + + GlowDataMap targetedMap = perPlayerGlows.get(Pair.of(player, entity)); + if (targetedMap == null) return data; + + GlowData targetedData = targetedMap.get(); + if (data == null) return targetedData; + + return data.compareTo(targetedData) < 0 ? data : targetedData; + } + } + + protected GlowData getGlowData(@NotNull Pair<@NotNull UUID, @NotNull UUID> pair) { + synchronized (this) { + GlowDataMap map = glows.get(pair.right()); + GlowData data = map == null ? null : map.get(); + + GlowDataMap targetedMap = perPlayerGlows.get(pair); + if (targetedMap == null) return data; + + GlowData targetedData = targetedMap.get(); + if (data == null) return targetedData; + + return data.compareTo(targetedData) < 0 ? data : targetedData; + } + } + + protected void sendPackets(@NotNull Player player, @NotNull Collection packets) { + if (packets.isEmpty()) return; + for (TPacket packet : packets) sendPacket(player, packet); + } + + protected ScoreboardEntry getScoreboardEntry(@Nullable Player player, @NotNull Entity entity) { + if (!libsDisguisesLoaded) return new ScoreboardEntry(entity.getScoreboardEntryName()); + return LibsDisguiseHelper.getDisguisedScoreboardEntry(player, entity); + } + + protected Iterable getTrackedBy(@NotNull Entity entity, @NotNull Set trackedBy) { + if (!(entity instanceof Player player) || trackedBy.contains(player)) return trackedBy; + return Iterables.concat(trackedBy, Collections.singleton(player)); + } + + protected T getStringOption(@NotNull String name, T def, @NotNull Function converter, @NotNull ConfigurationSection config, @NotNull Consumer onError) { + String string = config.getString("general.glow-spell-scoreboard-teams." + name); + if (string == null) return def; + + T value = converter.apply(string); + if (value != null) return value; + + onError.accept("Invalid value '" + string + "' for '" + name + "' in 'glow-spell-scoreboard-teams' in 'general.yml'."); + return def; + } + + protected Collection filterTeamEntries(@NotNull Player player, @NotNull Collection entries) { + UUID playerUUID = player.getUniqueId(); + + List filtered = new ArrayList<>(entries.size()); + boolean modified = false; + + for (String entry : entries) { + UUID entityUUID = scoreboardNames.get(entry); + if (entityUUID == null) { + filtered.add(entry); + continue; + } + + GlowData data = getGlowData(playerUUID, entityUUID); + if (data == null || data.color() == null) { + filtered.add(entry); + continue; + } + + modified = true; + } + + return modified ? filtered : null; + } + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + Player player = event.getPlayer(); + sendPackets(player, addTeamPackets); + + GlowData data = getGlowData(player.getUniqueId(), player.getUniqueId()); + if (data == null) return; + + updateGlow(player, player, null, data); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPlayerTrackEntity(PlayerTrackEntityEvent event) { + Player player = event.getPlayer(); + Entity entity = event.getEntity(); + + scheduleDelayedTask(() -> { + GlowData data = getGlowData(player.getUniqueId(), entity.getUniqueId()); + if (data == null) return; + + updateGlow(player, entity, null, data); + }, 0); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPlayerUntrackEntity(PlayerUntrackEntityEvent event) { + Player player = event.getPlayer(); + Entity entity = event.getEntity(); + + GlowData data = getGlowData(player.getUniqueId(), entity.getUniqueId()); + if (data == null) return; + + resetGlow(player, entity, data); + } + + protected abstract Collection createAddTeamPackets(); + + protected abstract Collection createRemoveTeamPackets(); + + protected abstract TEntityDataPacket createEntityDataPacket(@NotNull Entity entity, boolean forceGlow); + + protected abstract Collection createJoinTeamPacket(@NotNull GlowData data); + + protected abstract Collection createResetTeamPacket(@NotNull Scoreboard scoreboard, @NotNull GlowData data); + + protected abstract void sendPacket(@NotNull Player player, @NotNull TPacket packet); + + protected abstract void registerEvents(Listener listener); + + protected abstract void cancelTask(int taskId); + + public abstract int scheduleDelayedTask(Runnable runnable, long delay); + + public record ScoreboardEntry(@NotNull String realEntry, @Nullable String disguisedEntry) { + + public ScoreboardEntry { + disguisedEntry = realEntry.equals(disguisedEntry) ? null : disguisedEntry; + } + + public ScoreboardEntry(@NotNull String realEntry) { + this(realEntry, null); + } + + public Collection map(Function function) { + List list = new ArrayList<>(disguisedEntry == null ? 1 : 2); + if (disguisedEntry != null) list.add(function.apply(disguisedEntry)); + list.add(function.apply(realEntry)); + + return list; + } + + public void forEach(Consumer consumer) { + if (disguisedEntry != null) consumer.accept(disguisedEntry); + consumer.accept(realEntry); + } + + } + + protected static final class GlowData implements Comparable { + + private final NamedTextColor color; + private final int priority; + private final int sequence; + + private ScoreboardEntry lastScoreboardEntry; + private int taskId; + + public GlowData(int priority, int sequence, NamedTextColor color) { + this.priority = priority; + this.sequence = sequence; + this.color = color; + + taskId = -1; + } + + public NamedTextColor color() { + return color; + } + + public int priority() { + return priority; + } + + public int sequence() { + return sequence; + } + + public int taskId() { + return taskId; + } + + public void taskId(int taskId) { + this.taskId = taskId; + } + + public ScoreboardEntry lastScoreboardEntry() { + return lastScoreboardEntry; + } + + public void lastScoreboardEntry(ScoreboardEntry lastScoreboardEntry) { + this.lastScoreboardEntry = lastScoreboardEntry; + } + + @Override + public int compareTo(@NotNull GlowData o) { + int compare = o.priority - priority; + return compare == 0 ? o.sequence - sequence : compare; + } + + } + + protected static final class GlowDataMap { + + private final Map keyedGlowData = new Object2ObjectOpenHashMap<>(); + private final PriorityQueue glowData = new PriorityQueue<>(); + + public GlowDataMap() { + + } + + public GlowData put(NamespacedKey key, GlowData data) { + GlowData old = keyedGlowData.put(key, data); + if (old != null) glowData.remove(old); + + glowData.add(data); + + return old; + } + + public void remove(NamespacedKey key) { + GlowData old = keyedGlowData.remove(key); + if (old == null) return; + + glowData.remove(old); + } + + public GlowData get() { + return glowData.peek(); + } + + public boolean isEmpty() { + return keyedGlowData.isEmpty(); + } + + } + +} diff --git a/nms/shared/src/main/java/com/nisovin/magicspells/volatilecode/VolatileCodeHandle.java b/nms/shared/src/main/java/com/nisovin/magicspells/volatilecode/VolatileCodeHandle.java index 428671c7d..756e91a3c 100644 --- a/nms/shared/src/main/java/com/nisovin/magicspells/volatilecode/VolatileCodeHandle.java +++ b/nms/shared/src/main/java/com/nisovin/magicspells/volatilecode/VolatileCodeHandle.java @@ -1,5 +1,6 @@ package com.nisovin.magicspells.volatilecode; +import org.bukkit.World; import org.bukkit.entity.*; import org.bukkit.Location; import org.bukkit.util.Vector; @@ -9,6 +10,8 @@ import io.papermc.paper.advancement.AdvancementDisplay.Frame; +import com.nisovin.magicspells.util.glow.GlowManager; + public abstract class VolatileCodeHandle { protected final VolatileCodeHelper helper; @@ -35,4 +38,10 @@ public VolatileCodeHandle(VolatileCodeHelper helper) { public abstract void clearGameTestMarkers(Player player); + public abstract byte getEntityMetadata(Entity entity); + + public abstract Entity getEntityFromId(World world, int id); + + public abstract GlowManager getGlowManager(); + } diff --git a/nms/shared/src/main/java/com/nisovin/magicspells/volatilecode/VolatileCodeHelper.java b/nms/shared/src/main/java/com/nisovin/magicspells/volatilecode/VolatileCodeHelper.java index b1d630dd4..debc99fdb 100644 --- a/nms/shared/src/main/java/com/nisovin/magicspells/volatilecode/VolatileCodeHelper.java +++ b/nms/shared/src/main/java/com/nisovin/magicspells/volatilecode/VolatileCodeHelper.java @@ -1,9 +1,18 @@ package com.nisovin.magicspells.volatilecode; +import org.bukkit.event.Listener; +import org.bukkit.configuration.file.YamlConfiguration; + public interface VolatileCodeHelper { void error(String message); int scheduleDelayedTask(Runnable task, long delay); + void cancelTask(int id); + + void registerEvents(Listener listener); + + YamlConfiguration getMainConfig(); + }