diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/FancyVisuals.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/FancyVisuals.java index ec160dfc..7ed71dc6 100644 --- a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/FancyVisuals.java +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/FancyVisuals.java @@ -8,14 +8,36 @@ import de.oliver.fancyvisuals.analytics.AnalyticsManager; import de.oliver.fancyvisuals.api.FancyVisualsAPI; import de.oliver.fancyvisuals.api.nametags.NametagRepository; +import de.oliver.fancyvisuals.actionbar.ActionBarListeners; +import de.oliver.fancyvisuals.actionbar.ActionBarManager; +import de.oliver.fancyvisuals.actionbar.ActionBarRepository; +import de.oliver.fancyvisuals.actionbar.JsonActionBarRepository; +import de.oliver.fancyvisuals.bossbars.BossbarListeners; +import de.oliver.fancyvisuals.bossbars.BossbarManager; +import de.oliver.fancyvisuals.bossbars.BossbarRepository; +import de.oliver.fancyvisuals.bossbars.JsonBossbarRepository; +import de.oliver.fancyvisuals.chat.ChatFormatRepository; +import de.oliver.fancyvisuals.chat.ChatListeners; +import de.oliver.fancyvisuals.chat.JsonChatFormatRepository; +import de.oliver.fancyvisuals.commands.FancyVisualsCMD; import de.oliver.fancyvisuals.config.FancyVisualsConfig; import de.oliver.fancyvisuals.config.NametagConfig; +import de.oliver.fancyvisuals.nametags.NametagManager; import de.oliver.fancyvisuals.nametags.listeners.NametagListeners; import de.oliver.fancyvisuals.nametags.store.JsonNametagRepository; import de.oliver.fancyvisuals.nametags.visibility.PlayerNametagScheduler; import de.oliver.fancyvisuals.playerConfig.JsonPlayerConfigStore; +import de.oliver.fancyvisuals.tablist.JsonTablistRepository; +import de.oliver.fancyvisuals.tablist.TablistListeners; +import de.oliver.fancyvisuals.tablist.TablistManager; +import de.oliver.fancyvisuals.tablist.TablistRepository; +import de.oliver.fancyvisuals.titles.JsonTitleRepository; +import de.oliver.fancyvisuals.titles.TitleListeners; +import de.oliver.fancyvisuals.titles.TitleManager; +import de.oliver.fancyvisuals.titles.TitleRepository; import de.oliver.fancyvisuals.utils.VaultHelper; import org.bukkit.Bukkit; +import org.bukkit.command.Command; import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.java.JavaPlugin; import org.jetbrains.annotations.NotNull; @@ -36,6 +58,16 @@ public final class FancyVisuals extends JavaPlugin implements FancyVisualsAPI { private NametagRepository nametagRepository; private PlayerNametagScheduler nametagScheduler; + private NametagManager nametagManager; + private TablistRepository tablistRepository; + private TablistManager tablistManager; + private ActionBarRepository actionBarRepository; + private ActionBarManager actionBarManager; + private BossbarRepository bossbarRepository; + private BossbarManager bossbarManager; + private TitleRepository titleRepository; + private TitleManager titleManager; + private ChatFormatRepository chatFormatRepository; public FancyVisuals() { instance = this; @@ -55,11 +87,12 @@ public static FancyVisuals get() { @Override public void onLoad() { FancyLib fancyLib = new FancyLib(this); - IFancySitula.LOGGER.setCurrentLevel(LogLevel.DEBUG); + IFancySitula.LOGGER.setCurrentLevel(LogLevel.INFO); // config fancyVisualsConfig.load(); nametagConfig.load(); + IFancySitula.LOGGER.setCurrentLevel(parseLogLevel(fancyVisualsConfig.getLogLevel())); // worker executor this.workerExecutor = Executors.newFixedThreadPool( @@ -75,7 +108,31 @@ public void onLoad() { // Nametags nametagRepository = new JsonNametagRepository(); - nametagScheduler = new PlayerNametagScheduler(workerExecutor, nametagConfig.getDistributionBucketSize()); + nametagScheduler = new PlayerNametagScheduler( + workerExecutor, + nametagConfig.getDistributionBucketSize(), + nametagConfig.getUpdateIntervalMs() + ); + nametagManager = new NametagManager(nametagRepository, nametagScheduler); + + // Tablist + tablistRepository = new JsonTablistRepository(); + tablistManager = new TablistManager(this, tablistRepository); + + // Action bar + actionBarRepository = new JsonActionBarRepository(); + actionBarManager = new ActionBarManager(this, actionBarRepository); + + // Bossbars + bossbarRepository = new JsonBossbarRepository(); + bossbarManager = new BossbarManager(this, bossbarRepository); + + // Titles + titleRepository = new JsonTitleRepository(); + titleManager = new TitleManager(this, titleRepository); + + // Chat + chatFormatRepository = new JsonChatFormatRepository(); // analytics analyticsManager.init(); @@ -88,14 +145,69 @@ public void onEnable() { // Vault VaultHelper.loadVault(); - // Nametags - nametagScheduler.init(); + registerCommands(); + pluginManager.registerEvents(new NametagListeners(), this); + pluginManager.registerEvents(new TablistListeners(), this); + pluginManager.registerEvents(new ActionBarListeners(), this); + pluginManager.registerEvents(new BossbarListeners(), this); + pluginManager.registerEvents(new TitleListeners(), this); + pluginManager.registerEvents(new ChatListeners(), this); + + // Nametags + if (nametagConfig.isEnabled()) { + nametagScheduler.init(); + Bukkit.getOnlinePlayers().forEach(nametagManager::handlePlayerUpdate); + } + + if (fancyVisualsConfig.isTablistEnabled()) { + tablistManager.init(); + Bukkit.getOnlinePlayers().forEach(tablistManager::handleJoin); + } + + if (fancyVisualsConfig.isActionBarEnabled()) { + actionBarManager.init(); + Bukkit.getOnlinePlayers().forEach(actionBarManager::handleJoin); + } + + if (fancyVisualsConfig.isBossbarsEnabled()) { + bossbarManager.init(); + Bukkit.getOnlinePlayers().forEach(bossbarManager::handleJoin); + } + + if (fancyVisualsConfig.isTitlesEnabled()) { + titleManager.init(); + Bukkit.getOnlinePlayers().forEach(titleManager::handleJoin); + } } @Override public void onDisable() { - + if (nametagManager != null) { + nametagManager.shutdown(); + } + if (nametagScheduler != null) { + nametagScheduler.shutdown(); + } + if (tablistManager != null) { + tablistManager.clearAll(); + tablistManager.shutdown(); + } + if (actionBarManager != null) { + actionBarManager.clearAll(); + actionBarManager.shutdown(); + } + if (bossbarManager != null) { + bossbarManager.clearAll(); + bossbarManager.shutdown(); + } + if (titleManager != null) { + titleManager.clearAll(); + titleManager.shutdown(); + } + if (workerExecutor != null) { + workerExecutor.shutdownNow(); + } } @Override @@ -112,7 +224,146 @@ public NametagRepository getNametagRepository() { return nametagRepository; } + public NametagConfig getNametagConfig() { + return nametagConfig; + } + + public FancyVisualsConfig getFancyVisualsConfig() { + return fancyVisualsConfig; + } + public PlayerNametagScheduler getNametagScheduler() { return nametagScheduler; } + + public NametagManager getNametagManager() { + return nametagManager; + } + + public TablistRepository getTablistRepository() { + return tablistRepository; + } + + public TablistManager getTablistManager() { + return tablistManager; + } + + public ActionBarRepository getActionBarRepository() { + return actionBarRepository; + } + + public ActionBarManager getActionBarManager() { + return actionBarManager; + } + + public BossbarRepository getBossbarRepository() { + return bossbarRepository; + } + + public BossbarManager getBossbarManager() { + return bossbarManager; + } + + public TitleRepository getTitleRepository() { + return titleRepository; + } + + public TitleManager getTitleManager() { + return titleManager; + } + + public ChatFormatRepository getChatFormatRepository() { + return chatFormatRepository; + } + + public void reloadAll() { + fancyVisualsConfig.load(); + nametagConfig.load(); + IFancySitula.LOGGER.setCurrentLevel(parseLogLevel(fancyVisualsConfig.getLogLevel())); + VaultHelper.loadVault(); + + if (workerExecutor != null) { + workerExecutor.shutdownNow(); + } + this.workerExecutor = Executors.newFixedThreadPool( + fancyVisualsConfig.getAmountWorkerThreads(), + new ThreadFactoryBuilder() + .setNameFormat("FancyVisualsWorker-%d") + .build() + ); + + if (nametagManager != null) { + nametagManager.shutdown(); + } + if (nametagScheduler != null) { + nametagScheduler.shutdown(); + } + if (nametagConfig.isEnabled()) { + nametagScheduler = new PlayerNametagScheduler( + workerExecutor, + nametagConfig.getDistributionBucketSize(), + nametagConfig.getUpdateIntervalMs() + ); + nametagManager = new NametagManager(nametagRepository, nametagScheduler); + nametagScheduler.init(); + Bukkit.getOnlinePlayers().forEach(nametagManager::handlePlayerUpdate); + } + + if (tablistManager != null) { + tablistManager.clearAll(); + tablistManager.shutdown(); + } + if (fancyVisualsConfig.isTablistEnabled()) { + tablistManager = new TablistManager(this, tablistRepository); + tablistManager.init(); + Bukkit.getOnlinePlayers().forEach(tablistManager::handleJoin); + } + + if (actionBarManager != null) { + actionBarManager.clearAll(); + actionBarManager.shutdown(); + } + if (fancyVisualsConfig.isActionBarEnabled()) { + actionBarManager = new ActionBarManager(this, actionBarRepository); + actionBarManager.init(); + Bukkit.getOnlinePlayers().forEach(actionBarManager::handleJoin); + } + + if (bossbarManager != null) { + bossbarManager.clearAll(); + bossbarManager.shutdown(); + } + if (fancyVisualsConfig.isBossbarsEnabled()) { + bossbarManager = new BossbarManager(this, bossbarRepository); + bossbarManager.init(); + Bukkit.getOnlinePlayers().forEach(bossbarManager::handleJoin); + } + + if (titleManager != null) { + titleManager.clearAll(); + titleManager.shutdown(); + } + if (fancyVisualsConfig.isTitlesEnabled()) { + titleManager = new TitleManager(this, titleRepository); + titleManager.init(); + Bukkit.getOnlinePlayers().forEach(titleManager::handleJoin); + } + } + + private LogLevel parseLogLevel(String value) { + if (value == null) { + return LogLevel.INFO; + } + + try { + return LogLevel.valueOf(value.toUpperCase()); + } catch (IllegalArgumentException e) { + return LogLevel.INFO; + } + } + + private void registerCommands() { + Command command = new FancyVisualsCMD(this); + getServer().getCommandMap().register("fancyvisuals", command); + } } diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/actionbar/ActionBarConfig.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/actionbar/ActionBarConfig.java new file mode 100644 index 00000000..eb8a89cb --- /dev/null +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/actionbar/ActionBarConfig.java @@ -0,0 +1,14 @@ +package de.oliver.fancyvisuals.actionbar; + +import com.google.gson.annotations.SerializedName; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public record ActionBarConfig( + @SerializedName("messages") + @NotNull List messages, + @SerializedName("interval_ms") + int intervalMs +) { +} diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/actionbar/ActionBarListeners.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/actionbar/ActionBarListeners.java new file mode 100644 index 00000000..6275c8e4 --- /dev/null +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/actionbar/ActionBarListeners.java @@ -0,0 +1,42 @@ +package de.oliver.fancyvisuals.actionbar; + +import de.oliver.fancyvisuals.FancyVisuals; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerChangedWorldEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; + +public class ActionBarListeners implements Listener { + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + if (!FancyVisuals.get().getFancyVisualsConfig().isActionBarEnabled()) { + return; + } + + Player player = event.getPlayer(); + FancyVisuals.get().getActionBarManager().handleJoin(player); + } + + @EventHandler + public void onPlayerQuit(PlayerQuitEvent event) { + if (!FancyVisuals.get().getFancyVisualsConfig().isActionBarEnabled()) { + return; + } + + Player player = event.getPlayer(); + FancyVisuals.get().getActionBarManager().handleQuit(player); + } + + @EventHandler + public void onPlayerWorldChange(PlayerChangedWorldEvent event) { + if (!FancyVisuals.get().getFancyVisualsConfig().isActionBarEnabled()) { + return; + } + + Player player = event.getPlayer(); + FancyVisuals.get().getActionBarManager().handleContextChange(player); + } +} diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/actionbar/ActionBarManager.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/actionbar/ActionBarManager.java new file mode 100644 index 00000000..3564e954 --- /dev/null +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/actionbar/ActionBarManager.java @@ -0,0 +1,110 @@ +package de.oliver.fancyvisuals.actionbar; + +import de.oliver.fancyvisuals.FancyVisuals; +import de.oliver.fancyvisuals.utils.TextRenderer; +import net.kyori.adventure.text.Component; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public class ActionBarManager { + + private final FancyVisuals plugin; + private final ActionBarRepository repository; + private final ScheduledExecutorService scheduler; + private final Map states; + + public ActionBarManager(FancyVisuals plugin, ActionBarRepository repository) { + this.plugin = plugin; + this.repository = repository; + this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> new Thread(r, "ActionBarScheduler")); + this.states = new ConcurrentHashMap<>(); + } + + public void init() { + int intervalMs = plugin.getFancyVisualsConfig().getActionBarUpdateIntervalMs(); + scheduler.scheduleWithFixedDelay(this::queueUpdateCycle, intervalMs, intervalMs, TimeUnit.MILLISECONDS); + } + + public void shutdown() { + scheduler.shutdownNow(); + } + + public void clearAll() { + for (Player player : Bukkit.getOnlinePlayers()) { + player.sendActionBar(Component.empty()); + } + states.clear(); + } + + public void handleJoin(Player player) { + if (!plugin.getFancyVisualsConfig().isActionBarEnabled()) { + return; + } + + updateForPlayer(player); + } + + public void handleQuit(Player player) { + states.remove(player.getUniqueId()); + } + + public void handleContextChange(Player player) { + if (!plugin.getFancyVisualsConfig().isActionBarEnabled()) { + return; + } + + updateForPlayer(player); + } + + private void queueUpdateCycle() { + Bukkit.getScheduler().runTask(plugin, this::runUpdateCycle); + } + + private void runUpdateCycle() { + if (!plugin.getFancyVisualsConfig().isActionBarEnabled()) { + return; + } + + for (Player player : Bukkit.getOnlinePlayers()) { + updateForPlayer(player); + } + } + + private void updateForPlayer(Player player) { + ActionBarConfig config = repository.getActionBarForPlayer(player); + List messages = config.messages(); + if (messages.isEmpty()) { + player.sendActionBar(Component.empty()); + states.remove(player.getUniqueId()); + return; + } + + int intervalMs = config.intervalMs() > 0 ? config.intervalMs() : plugin.getFancyVisualsConfig().getActionBarUpdateIntervalMs(); + ActionBarState state = states.computeIfAbsent(player.getUniqueId(), key -> new ActionBarState()); + + long now = System.currentTimeMillis(); + if (now - state.lastSentAt < intervalMs) { + return; + } + + int index = state.index % messages.size(); + String message = messages.get(index); + player.sendActionBar(TextRenderer.render(message, player)); + + state.index = (state.index + 1) % messages.size(); + state.lastSentAt = now; + } + + private static class ActionBarState { + private long lastSentAt; + private int index; + } +} diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/actionbar/ActionBarRepository.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/actionbar/ActionBarRepository.java new file mode 100644 index 00000000..7eabd2d9 --- /dev/null +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/actionbar/ActionBarRepository.java @@ -0,0 +1,16 @@ +package de.oliver.fancyvisuals.actionbar; + +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public interface ActionBarRepository { + + ActionBarConfig DEFAULT_ACTIONBAR = new ActionBarConfig( + List.of("Welcome, %player_name%"), + 1000 + ); + + @NotNull ActionBarConfig getActionBarForPlayer(@NotNull Player player); +} diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/actionbar/JsonActionBarRepository.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/actionbar/JsonActionBarRepository.java new file mode 100644 index 00000000..b1f2bf9d --- /dev/null +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/actionbar/JsonActionBarRepository.java @@ -0,0 +1,44 @@ +package de.oliver.fancyvisuals.actionbar; + +import de.oliver.fancyvisuals.api.Context; +import de.oliver.fancyvisuals.utils.ContextLookup; +import de.oliver.fancyvisuals.utils.JsonContextStore; +import de.oliver.jdb.JDB; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class JsonActionBarRepository implements ActionBarRepository { + + private static final String BASE_PATH = "plugins/FancyVisuals/data/actionbar/"; + + private final Map> stores; + + public JsonActionBarRepository() { + stores = new ConcurrentHashMap<>(); + JDB jdb = new JDB(BASE_PATH); + + for (Context ctx : Context.values()) { + stores.put(ctx, new JsonContextStore<>(jdb, ctx, ActionBarConfig.class)); + } + + initialConfig(); + } + + @Override + public @NotNull ActionBarConfig getActionBarForPlayer(@NotNull Player player) { + return ContextLookup.resolve(player, (ctx, id) -> stores.get(ctx).get(id), DEFAULT_ACTIONBAR); + } + + private void initialConfig() { + File baseDir = new File(BASE_PATH); + if (baseDir.exists()) { + return; + } + + stores.get(Context.SERVER).set("global", DEFAULT_ACTIONBAR); + } +} diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/bossbars/BossbarConfig.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/bossbars/BossbarConfig.java new file mode 100644 index 00000000..76c20754 --- /dev/null +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/bossbars/BossbarConfig.java @@ -0,0 +1,25 @@ +package de.oliver.fancyvisuals.bossbars; + +import com.google.gson.annotations.SerializedName; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public record BossbarConfig( + @SerializedName("id") + @NotNull String id, + @SerializedName("text") + @NotNull String text, + @SerializedName("progress") + double progress, + @SerializedName("color") + @Nullable String color, + @SerializedName("style") + @Nullable String style, + @SerializedName("flags") + @NotNull List flags, + @SerializedName("visible") + boolean visible +) { +} diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/bossbars/BossbarListeners.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/bossbars/BossbarListeners.java new file mode 100644 index 00000000..e5e9eefa --- /dev/null +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/bossbars/BossbarListeners.java @@ -0,0 +1,42 @@ +package de.oliver.fancyvisuals.bossbars; + +import de.oliver.fancyvisuals.FancyVisuals; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerChangedWorldEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; + +public class BossbarListeners implements Listener { + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + if (!FancyVisuals.get().getFancyVisualsConfig().isBossbarsEnabled()) { + return; + } + + Player player = event.getPlayer(); + FancyVisuals.get().getBossbarManager().handleJoin(player); + } + + @EventHandler + public void onPlayerQuit(PlayerQuitEvent event) { + if (!FancyVisuals.get().getFancyVisualsConfig().isBossbarsEnabled()) { + return; + } + + Player player = event.getPlayer(); + FancyVisuals.get().getBossbarManager().handleQuit(player); + } + + @EventHandler + public void onPlayerWorldChange(PlayerChangedWorldEvent event) { + if (!FancyVisuals.get().getFancyVisualsConfig().isBossbarsEnabled()) { + return; + } + + Player player = event.getPlayer(); + FancyVisuals.get().getBossbarManager().handleContextChange(player); + } +} diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/bossbars/BossbarManager.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/bossbars/BossbarManager.java new file mode 100644 index 00000000..9070f73a --- /dev/null +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/bossbars/BossbarManager.java @@ -0,0 +1,152 @@ +package de.oliver.fancyvisuals.bossbars; + +import de.oliver.fancyvisuals.FancyVisuals; +import de.oliver.fancyvisuals.utils.TextRenderer; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.Bukkit; +import org.bukkit.boss.BarFlag; +import org.bukkit.boss.BossBar; +import org.bukkit.entity.Player; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public class BossbarManager { + + private static final LegacyComponentSerializer LEGACY_SERIALIZER = LegacyComponentSerializer.legacySection(); + + private final FancyVisuals plugin; + private final BossbarRepository repository; + private final ScheduledExecutorService scheduler; + private final Map> playerBars; + + public BossbarManager(FancyVisuals plugin, BossbarRepository repository) { + this.plugin = plugin; + this.repository = repository; + this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> new Thread(r, "BossbarScheduler")); + this.playerBars = new ConcurrentHashMap<>(); + } + + public void init() { + int intervalMs = plugin.getFancyVisualsConfig().getBossbarsUpdateIntervalMs(); + scheduler.scheduleWithFixedDelay(this::queueUpdateCycle, intervalMs, intervalMs, TimeUnit.MILLISECONDS); + } + + public void shutdown() { + scheduler.shutdownNow(); + } + + public void clearAll() { + for (Player player : Bukkit.getOnlinePlayers()) { + handleQuit(player); + } + playerBars.clear(); + } + + public void handleJoin(Player player) { + if (!plugin.getFancyVisualsConfig().isBossbarsEnabled()) { + return; + } + + updateForPlayer(player); + } + + public void handleQuit(Player player) { + Map bars = playerBars.remove(player.getUniqueId()); + if (bars != null) { + for (BossBar bar : bars.values()) { + bar.removePlayer(player); + } + } + } + + public void handleContextChange(Player player) { + if (!plugin.getFancyVisualsConfig().isBossbarsEnabled()) { + return; + } + + updateForPlayer(player); + } + + private void queueUpdateCycle() { + Bukkit.getScheduler().runTask(plugin, this::runUpdateCycle); + } + + private void runUpdateCycle() { + if (!plugin.getFancyVisualsConfig().isBossbarsEnabled()) { + return; + } + + for (Player player : Bukkit.getOnlinePlayers()) { + updateForPlayer(player); + } + } + + private void updateForPlayer(Player player) { + BossbarSet set = repository.getBossbarsForPlayer(player); + Map bars = playerBars.computeIfAbsent(player.getUniqueId(), key -> new HashMap<>()); + Set seen = new HashSet<>(); + + for (BossbarConfig config : set.bars()) { + if (config == null || config.id().isBlank()) { + continue; + } + + seen.add(config.id()); + + BossBar bar = bars.get(config.id()); + if (bar == null) { + Set flags = BossbarUtils.parseFlags(config.flags()); + bar = Bukkit.createBossBar("", BossbarUtils.parseColor(config.color()), BossbarUtils.parseStyle(config.style()), flags.toArray(new BarFlag[0])); + bars.put(config.id(), bar); + } + + Component title = TextRenderer.render(config.text(), player); + bar.setTitle(LEGACY_SERIALIZER.serialize(title)); + bar.setProgress(clampProgress(config.progress())); + bar.setColor(BossbarUtils.parseColor(config.color())); + bar.setStyle(BossbarUtils.parseStyle(config.style())); + + Set flags = BossbarUtils.parseFlags(config.flags()); + for (BarFlag flag : BarFlag.values()) { + if (flags.contains(flag)) { + bar.addFlag(flag); + } else { + bar.removeFlag(flag); + } + } + + bar.setVisible(config.visible()); + if (!bar.getPlayers().contains(player)) { + bar.addPlayer(player); + } + } + + Set toRemove = new HashSet<>(bars.keySet()); + toRemove.removeAll(seen); + for (String id : toRemove) { + BossBar bar = bars.remove(id); + if (bar != null) { + bar.removePlayer(player); + } + } + } + + private double clampProgress(double progress) { + if (progress < 0) { + return 0; + } + if (progress > 1) { + return 1; + } + return progress; + } +} diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/bossbars/BossbarRepository.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/bossbars/BossbarRepository.java new file mode 100644 index 00000000..1bd38198 --- /dev/null +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/bossbars/BossbarRepository.java @@ -0,0 +1,23 @@ +package de.oliver.fancyvisuals.bossbars; + +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public interface BossbarRepository { + + BossbarSet DEFAULT_BOSSBARS = new BossbarSet(List.of( + new BossbarConfig( + "announcement", + "FancyVisuals", + 1.0, + "BLUE", + "SOLID", + List.of(), + true + ) + )); + + @NotNull BossbarSet getBossbarsForPlayer(@NotNull Player player); +} diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/bossbars/BossbarSet.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/bossbars/BossbarSet.java new file mode 100644 index 00000000..58f23418 --- /dev/null +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/bossbars/BossbarSet.java @@ -0,0 +1,12 @@ +package de.oliver.fancyvisuals.bossbars; + +import com.google.gson.annotations.SerializedName; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public record BossbarSet( + @SerializedName("bars") + @NotNull List bars +) { +} diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/bossbars/BossbarUtils.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/bossbars/BossbarUtils.java new file mode 100644 index 00000000..5a215aaf --- /dev/null +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/bossbars/BossbarUtils.java @@ -0,0 +1,59 @@ +package de.oliver.fancyvisuals.bossbars; + +import org.bukkit.boss.BarColor; +import org.bukkit.boss.BarFlag; +import org.bukkit.boss.BarStyle; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public final class BossbarUtils { + + private BossbarUtils() { + } + + public static BarColor parseColor(String value) { + if (value == null) { + return BarColor.WHITE; + } + + try { + return BarColor.valueOf(value.toUpperCase()); + } catch (IllegalArgumentException e) { + return BarColor.WHITE; + } + } + + public static BarStyle parseStyle(String value) { + if (value == null) { + return BarStyle.SOLID; + } + + try { + return BarStyle.valueOf(value.toUpperCase()); + } catch (IllegalArgumentException e) { + return BarStyle.SOLID; + } + } + + public static Set parseFlags(List values) { + Set flags = new HashSet<>(); + if (values == null) { + return flags; + } + + for (String value : values) { + if (value == null) { + continue; + } + try { + flags.add(BarFlag.valueOf(value.toUpperCase())); + } catch (IllegalArgumentException ignored) { + // ignore invalid flag + } + } + + return flags; + } +} diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/bossbars/JsonBossbarRepository.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/bossbars/JsonBossbarRepository.java new file mode 100644 index 00000000..bcb882a1 --- /dev/null +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/bossbars/JsonBossbarRepository.java @@ -0,0 +1,44 @@ +package de.oliver.fancyvisuals.bossbars; + +import de.oliver.fancyvisuals.api.Context; +import de.oliver.fancyvisuals.utils.ContextLookup; +import de.oliver.fancyvisuals.utils.JsonContextStore; +import de.oliver.jdb.JDB; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class JsonBossbarRepository implements BossbarRepository { + + private static final String BASE_PATH = "plugins/FancyVisuals/data/bossbars/"; + + private final Map> stores; + + public JsonBossbarRepository() { + stores = new ConcurrentHashMap<>(); + JDB jdb = new JDB(BASE_PATH); + + for (Context ctx : Context.values()) { + stores.put(ctx, new JsonContextStore<>(jdb, ctx, BossbarSet.class)); + } + + initialConfig(); + } + + @Override + public @NotNull BossbarSet getBossbarsForPlayer(@NotNull Player player) { + return ContextLookup.resolve(player, (ctx, id) -> stores.get(ctx).get(id), DEFAULT_BOSSBARS); + } + + private void initialConfig() { + File baseDir = new File(BASE_PATH); + if (baseDir.exists()) { + return; + } + + stores.get(Context.SERVER).set("global", DEFAULT_BOSSBARS); + } +} diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/chat/ChatFormat.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/chat/ChatFormat.java new file mode 100644 index 00000000..1ad5ce2b --- /dev/null +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/chat/ChatFormat.java @@ -0,0 +1,10 @@ +package de.oliver.fancyvisuals.chat; + +import com.google.gson.annotations.SerializedName; +import org.jetbrains.annotations.NotNull; + +public record ChatFormat( + @SerializedName("format") + @NotNull String format +) { +} diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/chat/ChatFormatRepository.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/chat/ChatFormatRepository.java new file mode 100644 index 00000000..901ae869 --- /dev/null +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/chat/ChatFormatRepository.java @@ -0,0 +1,11 @@ +package de.oliver.fancyvisuals.chat; + +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +public interface ChatFormatRepository { + + ChatFormat DEFAULT_CHAT_FORMAT = new ChatFormat("%player_name% > {message}"); + + @NotNull ChatFormat getFormatForPlayer(@NotNull Player player); +} diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/chat/ChatListeners.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/chat/ChatListeners.java new file mode 100644 index 00000000..d7611add --- /dev/null +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/chat/ChatListeners.java @@ -0,0 +1,39 @@ +package de.oliver.fancyvisuals.chat; + +import de.oliver.fancyvisuals.FancyVisuals; +import de.oliver.fancyvisuals.utils.TextRenderer; +import io.papermc.paper.event.player.AsyncChatEvent; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextReplacementConfig; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; + +public class ChatListeners implements Listener { + + private static final String MESSAGE_TOKEN = "{message}"; + + @EventHandler + public void onAsyncChat(AsyncChatEvent event) { + if (!FancyVisuals.get().getFancyVisualsConfig().isChatEnabled()) { + return; + } + + Player player = event.getPlayer(); + ChatFormat format = FancyVisuals.get().getChatFormatRepository().getFormatForPlayer(player); + String rawFormat = format.format(); + Component base = TextRenderer.render(rawFormat, player); + boolean hasMessageToken = rawFormat.contains(MESSAGE_TOKEN); + + event.renderer((source, sourceDisplayName, message, viewer) -> { + if (hasMessageToken) { + return base.replaceText(TextReplacementConfig.builder() + .matchLiteral(MESSAGE_TOKEN) + .replacement(message) + .build()); + } + + return base.append(Component.space()).append(message); + }); + } +} diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/chat/JsonChatFormatRepository.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/chat/JsonChatFormatRepository.java new file mode 100644 index 00000000..0db28197 --- /dev/null +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/chat/JsonChatFormatRepository.java @@ -0,0 +1,44 @@ +package de.oliver.fancyvisuals.chat; + +import de.oliver.fancyvisuals.api.Context; +import de.oliver.fancyvisuals.utils.ContextLookup; +import de.oliver.fancyvisuals.utils.JsonContextStore; +import de.oliver.jdb.JDB; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class JsonChatFormatRepository implements ChatFormatRepository { + + private static final String BASE_PATH = "plugins/FancyVisuals/data/chat/"; + + private final Map> stores; + + public JsonChatFormatRepository() { + stores = new ConcurrentHashMap<>(); + JDB jdb = new JDB(BASE_PATH); + + for (Context ctx : Context.values()) { + stores.put(ctx, new JsonContextStore<>(jdb, ctx, ChatFormat.class)); + } + + initialConfig(); + } + + @Override + public @NotNull ChatFormat getFormatForPlayer(@NotNull Player player) { + return ContextLookup.resolve(player, (ctx, id) -> stores.get(ctx).get(id), DEFAULT_CHAT_FORMAT); + } + + private void initialConfig() { + File baseDir = new File(BASE_PATH); + if (baseDir.exists()) { + return; + } + + stores.get(Context.SERVER).set("global", DEFAULT_CHAT_FORMAT); + } +} diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/commands/FancyVisualsCMD.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/commands/FancyVisualsCMD.java new file mode 100644 index 00000000..698b2766 --- /dev/null +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/commands/FancyVisualsCMD.java @@ -0,0 +1,74 @@ +package de.oliver.fancyvisuals.commands; + +import de.oliver.fancyvisuals.FancyVisuals; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +public class FancyVisualsCMD extends Command { + + private static final String PERMISSION = "fancyvisuals.reload"; + private static final String LEGACY_PERMISSION = "fancyvisiuals.reload"; + + private final FancyVisuals plugin; + + public FancyVisualsCMD(FancyVisuals plugin) { + super("fancyvisuals"); + this.plugin = plugin; + setAliases(List.of("fv", "fancyvisiuals")); + setPermission(PERMISSION); + setDescription("FancyVisuals admin command"); + setUsage("/fancyvisuals reload"); + } + + @Override + public boolean execute(@NotNull CommandSender sender, @NotNull String label, @NotNull String[] args) { + if (args.length == 0) { + sender.sendMessage("Usage: /" + label + " reload"); + return true; + } + + String sub = args[0].toLowerCase(); + if ("reload".equals(sub) || "relaod".equals(sub)) { + if (!hasReloadPermission(sender)) { + sender.sendMessage("You do not have permission to do that."); + return true; + } + + plugin.reloadAll(); + sender.sendMessage("FancyVisuals reloaded."); + return true; + } + + sender.sendMessage("Unknown subcommand. Usage: /" + label + " reload"); + return true; + } + + @Override + public @NotNull List tabComplete(@NotNull CommandSender sender, @NotNull String alias, @NotNull String[] args) { + if (!hasReloadPermission(sender)) { + return List.of(); + } + + if (args.length == 1) { + String prefix = args[0].toLowerCase(); + List suggestions = new ArrayList<>(2); + if ("reload".startsWith(prefix)) { + suggestions.add("reload"); + } + if ("relaod".startsWith(prefix)) { + suggestions.add("relaod"); + } + return suggestions; + } + + return List.of(); + } + + private boolean hasReloadPermission(CommandSender sender) { + return sender.isOp() || sender.hasPermission(PERMISSION) || sender.hasPermission(LEGACY_PERMISSION); + } +} diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/config/FancyVisualsConfig.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/config/FancyVisualsConfig.java index b8535491..0e4b1409 100644 --- a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/config/FancyVisualsConfig.java +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/config/FancyVisualsConfig.java @@ -1,19 +1,214 @@ package de.oliver.fancyvisuals.config; +import com.fancyinnovations.config.Config; +import com.fancyinnovations.config.ConfigField; +import de.oliver.fancyvisuals.FancyVisuals; + public class FancyVisualsConfig { - private int amountWorkerThreads; + public static final String WORKER_THREADS_PATH = "settings.worker_threads"; + public static final String LOG_LEVEL_PATH = "settings.logging.level"; - public FancyVisualsConfig() { - this.amountWorkerThreads = 4; - } + public static final String TABLIST_ENABLED_PATH = "tablist.enabled"; + public static final String TABLIST_HEADER_FOOTER_ENABLED_PATH = "tablist.header_footer.enabled"; + public static final String TABLIST_ENTRIES_ENABLED_PATH = "tablist.entries.enabled"; + public static final String TABLIST_CUSTOM_ENTRIES_ENABLED_PATH = "tablist.custom_entries.enabled"; + public static final String TABLIST_UPDATE_INTERVAL_MS_PATH = "tablist.update_interval_ms"; + + public static final String ACTIONBAR_ENABLED_PATH = "actionbar.enabled"; + public static final String ACTIONBAR_UPDATE_INTERVAL_MS_PATH = "actionbar.update_interval_ms"; + + public static final String BOSSBARS_ENABLED_PATH = "bossbars.enabled"; + public static final String BOSSBARS_UPDATE_INTERVAL_MS_PATH = "bossbars.update_interval_ms"; + + public static final String TITLES_ENABLED_PATH = "titles.enabled"; + public static final String TITLES_UPDATE_INTERVAL_MS_PATH = "titles.update_interval_ms"; + + public static final String CHAT_ENABLED_PATH = "chat.enabled"; + + private static final String CONFIG_FILE_PATH = "plugins/FancyVisuals/config.yml"; + + private Config config; public void load() { - + if (config == null) { + config = new Config(FancyVisuals.getFancyLogger(), CONFIG_FILE_PATH); + + config.addField(new ConfigField<>( + WORKER_THREADS_PATH, + "Amount of worker threads used for FancyVisuals background tasks.", + false, + 4, + false, + Integer.class + )); + config.addField(new ConfigField<>( + LOG_LEVEL_PATH, + "The log level for the plugin (DEBUG, INFO, WARN, ERROR).", + false, + "INFO", + false, + String.class + )); + + config.addField(new ConfigField<>( + TABLIST_ENABLED_PATH, + "Enable the tablist module.", + false, + true, + false, + Boolean.class + )); + config.addField(new ConfigField<>( + TABLIST_HEADER_FOOTER_ENABLED_PATH, + "Enable tablist header & footer updates.", + false, + true, + false, + Boolean.class + )); + config.addField(new ConfigField<>( + TABLIST_ENTRIES_ENABLED_PATH, + "Enable player tablist entry formatting.", + false, + true, + false, + Boolean.class + )); + config.addField(new ConfigField<>( + TABLIST_CUSTOM_ENTRIES_ENABLED_PATH, + "Enable custom tablist entries.", + false, + true, + false, + Boolean.class + )); + config.addField(new ConfigField<>( + TABLIST_UPDATE_INTERVAL_MS_PATH, + "Interval in milliseconds to refresh tablist content.", + false, + 1000, + false, + Integer.class + )); + + config.addField(new ConfigField<>( + ACTIONBAR_ENABLED_PATH, + "Enable action bar updates.", + false, + true, + false, + Boolean.class + )); + config.addField(new ConfigField<>( + ACTIONBAR_UPDATE_INTERVAL_MS_PATH, + "Interval in milliseconds to refresh action bar content.", + false, + 1000, + false, + Integer.class + )); + + config.addField(new ConfigField<>( + BOSSBARS_ENABLED_PATH, + "Enable bossbar updates.", + false, + true, + false, + Boolean.class + )); + config.addField(new ConfigField<>( + BOSSBARS_UPDATE_INTERVAL_MS_PATH, + "Interval in milliseconds to refresh bossbar content.", + false, + 1000, + false, + Integer.class + )); + + config.addField(new ConfigField<>( + TITLES_ENABLED_PATH, + "Enable title and subtitle announcements.", + false, + true, + false, + Boolean.class + )); + config.addField(new ConfigField<>( + TITLES_UPDATE_INTERVAL_MS_PATH, + "Interval in milliseconds to refresh title announcements.", + false, + 5000, + false, + Integer.class + )); + + config.addField(new ConfigField<>( + CHAT_ENABLED_PATH, + "Enable chat formatting.", + false, + true, + false, + Boolean.class + )); + } + + config.reload(); } public int getAmountWorkerThreads() { - return amountWorkerThreads; + return config.get(WORKER_THREADS_PATH); + } + + public String getLogLevel() { + return config.get(LOG_LEVEL_PATH); + } + + public boolean isTablistEnabled() { + return config.get(TABLIST_ENABLED_PATH); + } + + public boolean isTablistHeaderFooterEnabled() { + return config.get(TABLIST_HEADER_FOOTER_ENABLED_PATH); + } + + public boolean isTablistEntriesEnabled() { + return config.get(TABLIST_ENTRIES_ENABLED_PATH); + } + + public boolean isTablistCustomEntriesEnabled() { + return config.get(TABLIST_CUSTOM_ENTRIES_ENABLED_PATH); + } + + public int getTablistUpdateIntervalMs() { + return config.get(TABLIST_UPDATE_INTERVAL_MS_PATH); + } + + public boolean isActionBarEnabled() { + return config.get(ACTIONBAR_ENABLED_PATH); + } + + public int getActionBarUpdateIntervalMs() { + return config.get(ACTIONBAR_UPDATE_INTERVAL_MS_PATH); + } + + public boolean isBossbarsEnabled() { + return config.get(BOSSBARS_ENABLED_PATH); + } + + public int getBossbarsUpdateIntervalMs() { + return config.get(BOSSBARS_UPDATE_INTERVAL_MS_PATH); } + public boolean isTitlesEnabled() { + return config.get(TITLES_ENABLED_PATH); + } + + public int getTitlesUpdateIntervalMs() { + return config.get(TITLES_UPDATE_INTERVAL_MS_PATH); + } + + public boolean isChatEnabled() { + return config.get(CHAT_ENABLED_PATH); + } } diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/config/NametagConfig.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/config/NametagConfig.java index fbda4c66..99898f87 100644 --- a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/config/NametagConfig.java +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/config/NametagConfig.java @@ -1,15 +1,85 @@ package de.oliver.fancyvisuals.config; +import com.fancyinnovations.config.Config; +import com.fancyinnovations.config.ConfigField; +import de.oliver.fancyvisuals.FancyVisuals; + public class NametagConfig { - private int distributionBucketSize; + public static final String ENABLED_PATH = "nametags.enabled"; + public static final String UPDATE_INTERVAL_MS_PATH = "nametags.update_interval_ms"; + public static final String BUCKET_SIZE_PATH = "nametags.bucket_size"; + public static final String MAX_DISTANCE_PATH = "nametags.max_distance"; + public static final String HIDE_VANILLA_PATH = "nametags.hide_vanilla_nametag"; + public static final String SHOW_OWN_NAMETAG_PATH = "nametags.show_own_nametag"; - public NametagConfig() { - distributionBucketSize = 10; - } + private static final String CONFIG_FILE_PATH = "plugins/FancyVisuals/config.yml"; + + private Config config; public void load() { + if (config == null) { + config = new Config(FancyVisuals.getFancyLogger(), CONFIG_FILE_PATH); + + config.addField(new ConfigField<>( + ENABLED_PATH, + "Enable the nametag module.", + false, + true, + false, + Boolean.class + )); + config.addField(new ConfigField<>( + UPDATE_INTERVAL_MS_PATH, + "Interval in milliseconds to refresh nametags.", + false, + 250, + false, + Integer.class + )); + config.addField(new ConfigField<>( + BUCKET_SIZE_PATH, + "Bucket count used to distribute nametag updates.", + false, + 10, + false, + Integer.class + )); + config.addField(new ConfigField<>( + MAX_DISTANCE_PATH, + "Maximum distance (in blocks) where nametags are visible.", + false, + 24, + false, + Integer.class + )); + config.addField(new ConfigField<>( + HIDE_VANILLA_PATH, + "Hide the vanilla nametag when FancyVisuals nametags are enabled.", + false, + true, + false, + Boolean.class + )); + config.addField(new ConfigField<>( + SHOW_OWN_NAMETAG_PATH, + "Whether players should see their own FancyVisuals nametag.", + false, + true, + false, + Boolean.class + )); + } + + config.reload(); + } + + public boolean isEnabled() { + return config.get(ENABLED_PATH); + } + public int getUpdateIntervalMs() { + return config.get(UPDATE_INTERVAL_MS_PATH); } /** @@ -18,6 +88,18 @@ public void load() { * @return The size of the distribution bucket. */ public int getDistributionBucketSize() { - return distributionBucketSize; + return config.get(BUCKET_SIZE_PATH); + } + + public int getMaxDistance() { + return config.get(MAX_DISTANCE_PATH); + } + + public boolean hideVanillaNametag() { + return config.get(HIDE_VANILLA_PATH); + } + + public boolean showOwnNametag() { + return config.get(SHOW_OWN_NAMETAG_PATH); } } diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/nametags/NametagManager.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/nametags/NametagManager.java new file mode 100644 index 00000000..11069c20 --- /dev/null +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/nametags/NametagManager.java @@ -0,0 +1,51 @@ +package de.oliver.fancyvisuals.nametags; + +import de.oliver.fancyvisuals.api.nametags.Nametag; +import de.oliver.fancyvisuals.api.nametags.NametagRepository; +import de.oliver.fancyvisuals.nametags.visibility.PlayerNametag; +import de.oliver.fancyvisuals.nametags.visibility.PlayerNametagScheduler; +import org.bukkit.entity.Player; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class NametagManager { + + private final NametagRepository repository; + private final PlayerNametagScheduler scheduler; + private final Map activeNametags; + + public NametagManager(NametagRepository repository, PlayerNametagScheduler scheduler) { + this.repository = repository; + this.scheduler = scheduler; + this.activeNametags = new ConcurrentHashMap<>(); + } + + public void handlePlayerUpdate(Player player) { + Nametag nametag = repository.getNametagForPlayer(player); + PlayerNametag existing = activeNametags.get(player.getUniqueId()); + + if (existing == null) { + PlayerNametag playerNametag = new PlayerNametag(nametag, player); + activeNametags.put(player.getUniqueId(), playerNametag); + scheduler.add(playerNametag); + } else { + existing.setNametag(nametag); + } + } + + public void handleQuit(Player player) { + PlayerNametag existing = activeNametags.remove(player.getUniqueId()); + if (existing != null) { + existing.hideFromAll(); + } + } + + public void shutdown() { + for (PlayerNametag nametag : activeNametags.values()) { + nametag.hideFromAll(); + } + activeNametags.clear(); + } +} diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/nametags/listeners/NametagListeners.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/nametags/listeners/NametagListeners.java index 0e3b095f..fbdb12a9 100644 --- a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/nametags/listeners/NametagListeners.java +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/nametags/listeners/NametagListeners.java @@ -1,29 +1,42 @@ package de.oliver.fancyvisuals.nametags.listeners; import de.oliver.fancyvisuals.FancyVisuals; -import de.oliver.fancyvisuals.api.nametags.Nametag; -import de.oliver.fancyvisuals.nametags.visibility.PlayerNametag; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerChangedWorldEvent; import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; public class NametagListeners implements Listener { @EventHandler public void onPlayerJoin(PlayerJoinEvent event) { + if (!FancyVisuals.get().getNametagConfig().isEnabled()) { + return; + } + Player player = event.getPlayer(); - Nametag nametag = FancyVisuals.get().getNametagRepository().getNametagForPlayer(player); - PlayerNametag playerNametag = new PlayerNametag(nametag, player); - FancyVisuals.get().getNametagScheduler().add(playerNametag); + FancyVisuals.get().getNametagManager().handlePlayerUpdate(player); } @EventHandler public void onPlayerWorldChange(PlayerChangedWorldEvent event) { + if (!FancyVisuals.get().getNametagConfig().isEnabled()) { + return; + } + + Player player = event.getPlayer(); + FancyVisuals.get().getNametagManager().handlePlayerUpdate(player); + } + + @EventHandler + public void onPlayerQuit(PlayerQuitEvent event) { + if (!FancyVisuals.get().getNametagConfig().isEnabled()) { + return; + } + Player player = event.getPlayer(); - Nametag nametag = FancyVisuals.get().getNametagRepository().getNametagForPlayer(player); - PlayerNametag playerNametag = new PlayerNametag(nametag, player); - FancyVisuals.get().getNametagScheduler().add(playerNametag); + FancyVisuals.get().getNametagManager().handleQuit(player); } } diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/nametags/store/JsonNametagStore.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/nametags/store/JsonNametagStore.java index cf82ab8f..3ab57c83 100644 --- a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/nametags/store/JsonNametagStore.java +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/nametags/store/JsonNametagStore.java @@ -52,14 +52,12 @@ public void removeNametag(@NotNull String id) { @Override public @NotNull List getNametags() { - List nametags = new ArrayList<>(); - try { - jdb.getAll(context.getName(), Nametag.class); + return jdb.getAll(context.getName(), Nametag.class); } catch (IOException e) { FancyVisuals.getFancyLogger().error("Failed to get all nametags", ThrowableProperty.of(e)); } - return nametags; + return new ArrayList<>(); } } diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/nametags/visibility/PlayerNametag.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/nametags/visibility/PlayerNametag.java index 33df40ed..9f17d758 100644 --- a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/nametags/visibility/PlayerNametag.java +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/nametags/visibility/PlayerNametag.java @@ -7,12 +7,12 @@ import de.oliver.fancyvisuals.FancyVisuals; import de.oliver.fancyvisuals.api.nametags.Nametag; import de.oliver.fancyvisuals.playerConfig.PlayerConfig; +import de.oliver.fancyvisuals.utils.TextRenderer; import org.bukkit.Bukkit; import org.bukkit.Color; import org.bukkit.Location; import org.bukkit.entity.Player; import org.joml.Vector3f; -import org.lushplugins.chatcolorhandler.ModernChatColorHandler; import java.util.HashSet; import java.util.List; @@ -21,7 +21,7 @@ public class PlayerNametag { - private final Nametag nametag; + private Nametag nametag; private final Player player; private final Set viewers; private FS_TextDisplay fsTextDisplay; @@ -44,6 +44,8 @@ public void updateVisibilityForAll() { showTo(viewer); } else if (!should && is) { hideFrom(viewer); + } else if (should) { + updateFor(viewer); } } @@ -59,6 +61,9 @@ private boolean shouldBeVisibleTo(Player viewer) { } if (player.getUniqueId().equals(viewer.getUniqueId())) { + if (!FancyVisuals.get().getNametagConfig().showOwnNametag()) { + return false; + } PlayerConfig playerConfig = FancyVisuals.get().getPlayerConfigStore().getPlayerConfig(player.getUniqueId()); if (!playerConfig.showOwnNametag()) { return false; @@ -70,7 +75,8 @@ private boolean shouldBeVisibleTo(Player viewer) { return false; } - boolean inDistance = isInDistance(viewer.getLocation(), player.getLocation(), 24); + int maxDistance = FancyVisuals.get().getNametagConfig().getMaxDistance(); + boolean inDistance = isInDistance(viewer.getLocation(), player.getLocation(), maxDistance); if (!inDistance) { return false; } @@ -94,13 +100,22 @@ public void hideFrom(Player viewer) { FancySitula.ENTITY_FACTORY.despawnEntityFor(fsViewer, fsTextDisplay); } + public void hideFromAll() { + Set snapshot = new HashSet<>(viewers); + for (UUID viewerId : snapshot) { + Player viewer = Bukkit.getPlayer(viewerId); + if (viewer != null) { + hideFrom(viewer); + } + } + } + public void updateFor(Player viewer) { fsTextDisplay.setTranslation(new Vector3f(0, 0.2f, 0)); fsTextDisplay.setBillboard(FS_Display.Billboard.CENTER); - Color bgColor = Color.fromARGB((int) Long.parseLong(nametag.backgroundColor().substring(1), 16)); - fsTextDisplay.setBackground(bgColor.asARGB()); + fsTextDisplay.setBackground(parseBackground(nametag.backgroundColor())); fsTextDisplay.setStyleFlags((byte) 0); @@ -115,13 +130,7 @@ public void updateFor(Player viewer) { } } - StringBuilder text = new StringBuilder(); - for (String line : nametag.textLines()) { - text.append(line).append('\n'); - } - text.deleteCharAt(text.length() - 1); - - fsTextDisplay.setText(ModernChatColorHandler.translate(text.toString(), player)); + fsTextDisplay.setText(TextRenderer.renderLines(nametag.textLines(), player)); FS_RealPlayer fsViewer = new FS_RealPlayer(viewer); FancySitula.ENTITY_FACTORY.setEntityDataFor(fsViewer, fsTextDisplay); @@ -151,6 +160,10 @@ public Nametag getNametag() { return nametag; } + public void setNametag(Nametag nametag) { + this.nametag = nametag; + } + public Player getPlayer() { return player; } @@ -162,4 +175,22 @@ public Set getViewers() { private boolean isInDistance(Location loc1, Location loc2, double distance) { return loc1.distanceSquared(loc2) <= distance * distance; } + + private int parseBackground(String hex) { + if (hex == null || hex.isEmpty()) { + return Color.fromARGB(0).asARGB(); + } + + String normalized = hex.startsWith("#") ? hex.substring(1) : hex; + if (normalized.length() == 6) { + normalized = "FF" + normalized; + } + + try { + long value = Long.parseLong(normalized, 16); + return Color.fromARGB((int) value).asARGB(); + } catch (NumberFormatException e) { + return Color.fromARGB(0).asARGB(); + } + } } diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/nametags/visibility/PlayerNametagScheduler.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/nametags/visibility/PlayerNametagScheduler.java index 8a029d30..24fa1be4 100644 --- a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/nametags/visibility/PlayerNametagScheduler.java +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/nametags/visibility/PlayerNametagScheduler.java @@ -25,12 +25,15 @@ public class PlayerNametagScheduler { */ private final DistributedWorkload workload; - public PlayerNametagScheduler(ExecutorService workerExecutor, int bucketSize) { + private final long updateIntervalMs; + + public PlayerNametagScheduler(ExecutorService workerExecutor, int bucketSize, long updateIntervalMs) { this.schedulerExecutor = Executors.newSingleThreadScheduledExecutor( new ThreadFactoryBuilder() .setNameFormat("PlayerNametagScheduler") .build() ); + this.updateIntervalMs = updateIntervalMs; this.workload = new DistributedWorkload<>( "PlayerNametagWorkload", @@ -48,10 +51,14 @@ public PlayerNametagScheduler(ExecutorService workerExecutor, int bucketSize) { * 25 seconds between subsequent executions. */ public void init() { - schedulerExecutor.scheduleWithFixedDelay(workload, 1000, 250, TimeUnit.MILLISECONDS); + schedulerExecutor.scheduleWithFixedDelay(workload, updateIntervalMs, updateIntervalMs, TimeUnit.MILLISECONDS); } public void add(PlayerNametag nametag) { workload.addValue(() -> nametag); } + + public void shutdown() { + schedulerExecutor.shutdownNow(); + } } diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/tablist/JsonTablistRepository.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/tablist/JsonTablistRepository.java new file mode 100644 index 00000000..1c22e3d1 --- /dev/null +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/tablist/JsonTablistRepository.java @@ -0,0 +1,99 @@ +package de.oliver.fancyvisuals.tablist; + +import de.oliver.fancyvisuals.api.Context; +import de.oliver.fancyvisuals.tablist.data.CustomTablistEntries; +import de.oliver.fancyvisuals.tablist.data.CustomTablistEntry; +import de.oliver.fancyvisuals.tablist.data.TablistHeaderFooter; +import de.oliver.fancyvisuals.tablist.data.TablistPlayerFormat; +import de.oliver.fancyvisuals.utils.ContextLookup; +import de.oliver.fancyvisuals.utils.JsonContextStore; +import de.oliver.jdb.JDB; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class JsonTablistRepository implements TablistRepository { + + private static final String HEADER_FOOTER_BASE = "plugins/FancyVisuals/data/tablist/header-footer/"; + private static final String PLAYER_FORMAT_BASE = "plugins/FancyVisuals/data/tablist/player-format/"; + private static final String CUSTOM_ENTRIES_BASE = "plugins/FancyVisuals/data/tablist/custom-entries/"; + + private final Map> headerStores; + private final Map> formatStores; + private final Map> customStores; + + public JsonTablistRepository() { + headerStores = new ConcurrentHashMap<>(); + formatStores = new ConcurrentHashMap<>(); + customStores = new ConcurrentHashMap<>(); + + JDB headerDb = new JDB(HEADER_FOOTER_BASE); + JDB formatDb = new JDB(PLAYER_FORMAT_BASE); + JDB customDb = new JDB(CUSTOM_ENTRIES_BASE); + + for (Context ctx : Context.values()) { + headerStores.put(ctx, new JsonContextStore<>(headerDb, ctx, TablistHeaderFooter.class)); + formatStores.put(ctx, new JsonContextStore<>(formatDb, ctx, TablistPlayerFormat.class)); + customStores.put(ctx, new JsonContextStore<>(customDb, ctx, CustomTablistEntries.class)); + } + + initialConfig(); + } + + @Override + public @NotNull TablistHeaderFooter getHeaderFooterForPlayer(@NotNull Player player) { + return ContextLookup.resolve(player, (ctx, id) -> headerStores.get(ctx).get(id), DEFAULT_HEADER_FOOTER); + } + + @Override + public @NotNull TablistPlayerFormat getPlayerFormatForPlayer(@NotNull Player player) { + return ContextLookup.resolve(player, (ctx, id) -> formatStores.get(ctx).get(id), DEFAULT_PLAYER_FORMAT); + } + + @Override + public @NotNull CustomTablistEntries getCustomEntriesForPlayer(@NotNull Player player) { + return ContextLookup.resolve(player, (ctx, id) -> customStores.get(ctx).get(id), DEFAULT_CUSTOM_ENTRIES); + } + + private void initialConfig() { + File headerDir = new File(HEADER_FOOTER_BASE); + File formatDir = new File(PLAYER_FORMAT_BASE); + File customDir = new File(CUSTOM_ENTRIES_BASE); + + if (headerDir.exists() || formatDir.exists() || customDir.exists()) { + return; + } + + headerStores.get(Context.SERVER).set("global", DEFAULT_HEADER_FOOTER); + + formatStores.get(Context.SERVER).set("global", DEFAULT_PLAYER_FORMAT); + formatStores.get(Context.GROUP).set("admin", new TablistPlayerFormat( + "[Admin] ", + "%player_name%", + "", + 10 + )); + formatStores.get(Context.GROUP).set("moderator", new TablistPlayerFormat( + "[Mod] ", + "%player_name%", + "", + 20 + )); + + customStores.get(Context.SERVER).set("global", new CustomTablistEntries(List.of( + new CustomTablistEntry( + "website", + "website: example.com", + 5, + 0, + "SURVIVAL", + "fv_website", + null + ) + ))); + } +} diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/tablist/TablistListeners.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/tablist/TablistListeners.java new file mode 100644 index 00000000..c8b56652 --- /dev/null +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/tablist/TablistListeners.java @@ -0,0 +1,42 @@ +package de.oliver.fancyvisuals.tablist; + +import de.oliver.fancyvisuals.FancyVisuals; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerChangedWorldEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; + +public class TablistListeners implements Listener { + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + if (!FancyVisuals.get().getFancyVisualsConfig().isTablistEnabled()) { + return; + } + + Player player = event.getPlayer(); + FancyVisuals.get().getTablistManager().handleJoin(player); + } + + @EventHandler + public void onPlayerQuit(PlayerQuitEvent event) { + if (!FancyVisuals.get().getFancyVisualsConfig().isTablistEnabled()) { + return; + } + + Player player = event.getPlayer(); + FancyVisuals.get().getTablistManager().handleQuit(player); + } + + @EventHandler + public void onPlayerWorldChange(PlayerChangedWorldEvent event) { + if (!FancyVisuals.get().getFancyVisualsConfig().isTablistEnabled()) { + return; + } + + Player player = event.getPlayer(); + FancyVisuals.get().getTablistManager().handleContextChange(player); + } +} diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/tablist/TablistManager.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/tablist/TablistManager.java new file mode 100644 index 00000000..a813b676 --- /dev/null +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/tablist/TablistManager.java @@ -0,0 +1,420 @@ +package de.oliver.fancyvisuals.tablist; + +import de.oliver.fancysitula.api.entities.FS_RealPlayer; +import de.oliver.fancysitula.api.packets.FS_ClientboundPlayerInfoRemovePacket; +import de.oliver.fancysitula.api.packets.FS_ClientboundPlayerInfoUpdatePacket; +import de.oliver.fancysitula.api.utils.FS_GameProfile; +import de.oliver.fancysitula.api.utils.FS_GameType; +import de.oliver.fancysitula.factories.FancySitula; +import de.oliver.fancyvisuals.FancyVisuals; +import de.oliver.fancyvisuals.tablist.data.CustomTablistEntries; +import de.oliver.fancyvisuals.tablist.data.CustomTablistEntry; +import de.oliver.fancyvisuals.tablist.data.TablistHeaderFooter; +import de.oliver.fancyvisuals.tablist.data.TablistPlayerFormat; +import de.oliver.fancyvisuals.tablist.data.TablistSkin; +import de.oliver.fancyvisuals.utils.TextRenderer; +import net.kyori.adventure.text.Component; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public class TablistManager { + + private final FancyVisuals plugin; + private final TablistRepository repository; + private final TablistTeamManager teamManager; + private final ScheduledExecutorService scheduler; + + private final Map headerFooterCache; + private final Map playerEntryCache; + private final Map playerSortCache; + private final Map> customEntryCache; + + public TablistManager(FancyVisuals plugin, TablistRepository repository) { + this.plugin = plugin; + this.repository = repository; + this.teamManager = new TablistTeamManager(); + this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> new Thread(r, "TablistScheduler")); + + this.headerFooterCache = new ConcurrentHashMap<>(); + this.playerEntryCache = new ConcurrentHashMap<>(); + this.playerSortCache = new ConcurrentHashMap<>(); + this.customEntryCache = new ConcurrentHashMap<>(); + } + + public void init() { + int intervalMs = plugin.getFancyVisualsConfig().getTablistUpdateIntervalMs(); + scheduler.scheduleWithFixedDelay(this::queueUpdateCycle, intervalMs, intervalMs, TimeUnit.MILLISECONDS); + } + + public void shutdown() { + scheduler.shutdownNow(); + } + + public void clearAll() { + for (Player viewer : Bukkit.getOnlinePlayers()) { + Map states = customEntryCache.get(viewer.getUniqueId()); + if (states != null) { + for (CustomEntryState state : states.values()) { + removeCustomEntry(viewer, state); + } + } + teamManager.removeViewer(viewer); + } + customEntryCache.clear(); + headerFooterCache.clear(); + playerEntryCache.clear(); + playerSortCache.clear(); + } + + public void handleJoin(Player player) { + if (!plugin.getFancyVisualsConfig().isTablistEnabled()) { + return; + } + + if (plugin.getFancyVisualsConfig().isTablistHeaderFooterEnabled()) { + updateHeaderFooter(player); + } + + if (plugin.getFancyVisualsConfig().isTablistCustomEntriesEnabled()) { + updateCustomEntries(player); + } + + if (plugin.getFancyVisualsConfig().isTablistEntriesEnabled()) { + sendAllEntriesToViewer(player); + updatePlayerEntry(player); + } else if (shouldAssignTeams()) { + assignTeamsForViewer(player); + } + } + + public void handleQuit(Player player) { + headerFooterCache.remove(player.getUniqueId()); + customEntryCache.remove(player.getUniqueId()); + teamManager.removeViewer(player); + } + + public void handleContextChange(Player player) { + if (!plugin.getFancyVisualsConfig().isTablistEnabled()) { + return; + } + + if (plugin.getFancyVisualsConfig().isTablistHeaderFooterEnabled()) { + updateHeaderFooter(player); + } + + if (plugin.getFancyVisualsConfig().isTablistCustomEntriesEnabled()) { + updateCustomEntries(player); + } + + if (plugin.getFancyVisualsConfig().isTablistEntriesEnabled()) { + updatePlayerEntry(player); + } else if (shouldAssignTeams()) { + updatePlayerTeams(player); + } + } + + private void queueUpdateCycle() { + Bukkit.getScheduler().runTask(plugin, this::runUpdateCycle); + } + + private void runUpdateCycle() { + if (!plugin.getFancyVisualsConfig().isTablistEnabled()) { + return; + } + + if (plugin.getFancyVisualsConfig().isTablistHeaderFooterEnabled()) { + for (Player player : Bukkit.getOnlinePlayers()) { + updateHeaderFooter(player); + } + } + + if (plugin.getFancyVisualsConfig().isTablistCustomEntriesEnabled()) { + for (Player player : Bukkit.getOnlinePlayers()) { + updateCustomEntries(player); + } + } + + if (plugin.getFancyVisualsConfig().isTablistEntriesEnabled()) { + for (Player player : Bukkit.getOnlinePlayers()) { + updatePlayerEntry(player); + } + } else if (shouldAssignTeams()) { + for (Player player : Bukkit.getOnlinePlayers()) { + updatePlayerTeams(player); + } + } + } + + private void updateHeaderFooter(Player player) { + TablistHeaderFooter data = repository.getHeaderFooterForPlayer(player); + Component header = TextRenderer.render(data.header(), player); + Component footer = TextRenderer.render(data.footer(), player); + + HeaderFooterState cached = headerFooterCache.get(player.getUniqueId()); + if (cached != null && cached.header.equals(header) && cached.footer.equals(footer)) { + return; + } + + player.sendPlayerListHeaderAndFooter(header, footer); + headerFooterCache.put(player.getUniqueId(), new HeaderFooterState(header, footer)); + } + + private void updatePlayerEntry(Player player) { + TablistPlayerFormat format = repository.getPlayerFormatForPlayer(player); + Component display = buildDisplayName(format, player); + + Component cachedDisplay = playerEntryCache.get(player.getUniqueId()); + if (cachedDisplay == null || !cachedDisplay.equals(display)) { + sendDisplayUpdateToAllViewers(player, display); + playerEntryCache.put(player.getUniqueId(), display); + } + + int sortPriority = format.sortPriority(); + Integer cachedSort = playerSortCache.get(player.getUniqueId()); + if (cachedSort == null || cachedSort != sortPriority) { + assignSortTeamToAllViewers(player, sortPriority); + playerSortCache.put(player.getUniqueId(), sortPriority); + } + } + + private void sendAllEntriesToViewer(Player viewer) { + for (Player target : Bukkit.getOnlinePlayers()) { + TablistPlayerFormat format = repository.getPlayerFormatForPlayer(target); + Component display = playerEntryCache.computeIfAbsent(target.getUniqueId(), key -> buildDisplayName(format, target)); + sendDisplayUpdate(viewer, target, display); + + int sortPriority = format.sortPriority(); + playerSortCache.putIfAbsent(target.getUniqueId(), sortPriority); + teamManager.assignEntity(viewer, target.getName(), sortPriority); + } + } + + private void assignTeamsForViewer(Player viewer) { + for (Player target : Bukkit.getOnlinePlayers()) { + TablistPlayerFormat format = repository.getPlayerFormatForPlayer(target); + int sortPriority = format.sortPriority(); + playerSortCache.putIfAbsent(target.getUniqueId(), sortPriority); + teamManager.assignEntity(viewer, target.getName(), sortPriority); + } + } + + private void assignSortTeamToAllViewers(Player target, int sortPriority) { + for (Player viewer : Bukkit.getOnlinePlayers()) { + teamManager.assignEntity(viewer, target.getName(), sortPriority); + } + } + + private void sendDisplayUpdateToAllViewers(Player target, Component display) { + FS_ClientboundPlayerInfoUpdatePacket packet = buildPlayerInfoUpdate(target, display); + for (Player viewer : Bukkit.getOnlinePlayers()) { + packet.send(new FS_RealPlayer(viewer)); + } + } + + private void sendDisplayUpdate(Player viewer, Player target, Component display) { + FS_ClientboundPlayerInfoUpdatePacket packet = buildPlayerInfoUpdate(target, display); + packet.send(new FS_RealPlayer(viewer)); + } + + private void updatePlayerTeams(Player player) { + TablistPlayerFormat format = repository.getPlayerFormatForPlayer(player); + int sortPriority = format.sortPriority(); + Integer cachedSort = playerSortCache.get(player.getUniqueId()); + if (cachedSort == null || cachedSort != sortPriority) { + assignSortTeamToAllViewers(player, sortPriority); + playerSortCache.put(player.getUniqueId(), sortPriority); + } + } + + private FS_ClientboundPlayerInfoUpdatePacket buildPlayerInfoUpdate(Player target, Component display) { + FS_GameProfile profile = TablistUtils.toProfile(target); + FS_GameType gameMode = TablistUtils.toGameType(target.getGameMode()); + int ping = Math.max(0, target.getPing()); + + FS_ClientboundPlayerInfoUpdatePacket.Entry entry = new FS_ClientboundPlayerInfoUpdatePacket.Entry( + target.getUniqueId(), + profile, + true, + ping, + gameMode, + display + ); + + return FancySitula.PACKET_FACTORY.createPlayerInfoUpdatePacket( + EnumSet.of(FS_ClientboundPlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME), + List.of(entry) + ); + } + + private Component buildDisplayName(TablistPlayerFormat format, Player player) { + String prefix = format.prefix() == null ? "" : format.prefix(); + String name = (format.name() == null || format.name().isBlank()) ? "%player_name%" : format.name(); + String suffix = format.suffix() == null ? "" : format.suffix(); + String raw = prefix + name + suffix; + return TextRenderer.render(raw, player); + } + + private boolean shouldAssignTeams() { + return plugin.getNametagConfig().hideVanillaNametag(); + } + + private void updateCustomEntries(Player viewer) { + CustomTablistEntries entries = repository.getCustomEntriesForPlayer(viewer); + Map states = customEntryCache.computeIfAbsent(viewer.getUniqueId(), key -> new HashMap<>()); + Set seen = new HashSet<>(); + + for (CustomTablistEntry entry : entries.entries()) { + if (entry == null || entry.id().isBlank()) { + continue; + } + + seen.add(entry.id()); + + String profileName = TablistUtils.buildProfileName(entry); + UUID entryUuid = TablistUtils.buildEntryUuid(viewer, entry); + Component display = TextRenderer.render(entry.text(), viewer); + + CustomEntryState state = states.get(entry.id()); + if (state == null || !state.profileName.equals(profileName) || !skinsMatch(state, entry)) { + if (state != null) { + removeCustomEntry(viewer, state); + } + + addCustomEntry(viewer, entry, entryUuid, profileName, display); + states.put(entry.id(), new CustomEntryState(entryUuid, profileName, display, entry.sortPriority(), entry.skin())); + } else { + if (!state.display.equals(display)) { + updateCustomEntryDisplay(viewer, entryUuid, profileName, entry, display); + state.display = display; + } + + if (state.sortPriority != entry.sortPriority()) { + state.sortPriority = entry.sortPriority(); + teamManager.assignEntity(viewer, profileName, entry.sortPriority()); + } else { + teamManager.assignEntity(viewer, profileName, entry.sortPriority()); + } + } + } + + Set toRemove = new HashSet<>(states.keySet()); + toRemove.removeAll(seen); + for (String entryId : toRemove) { + CustomEntryState state = states.remove(entryId); + if (state != null) { + removeCustomEntry(viewer, state); + } + } + } + + private void addCustomEntry(Player viewer, CustomTablistEntry entry, UUID uuid, String profileName, Component display) { + FS_GameProfile profile = new FS_GameProfile(uuid, profileName); + if (entry.skin() != null) { + entry.skin().applyTo(profile); + } + + FS_GameType gameMode = TablistUtils.toGameType(entry.gameMode()); + int ping = Math.max(0, entry.ping()); + + FS_ClientboundPlayerInfoUpdatePacket.Entry packetEntry = new FS_ClientboundPlayerInfoUpdatePacket.Entry( + uuid, + profile, + true, + ping, + gameMode, + display + ); + + FS_ClientboundPlayerInfoUpdatePacket packet = FancySitula.PACKET_FACTORY.createPlayerInfoUpdatePacket( + EnumSet.of( + FS_ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER, + FS_ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LISTED, + FS_ClientboundPlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME + ), + List.of(packetEntry) + ); + + packet.send(new FS_RealPlayer(viewer)); + teamManager.assignEntity(viewer, profileName, entry.sortPriority()); + } + + private void updateCustomEntryDisplay(Player viewer, UUID uuid, String profileName, CustomTablistEntry entry, Component display) { + FS_GameProfile profile = new FS_GameProfile(uuid, profileName); + if (entry.skin() != null) { + entry.skin().applyTo(profile); + } + + FS_GameType gameMode = TablistUtils.toGameType(entry.gameMode()); + int ping = Math.max(0, entry.ping()); + + FS_ClientboundPlayerInfoUpdatePacket.Entry packetEntry = new FS_ClientboundPlayerInfoUpdatePacket.Entry( + uuid, + profile, + true, + ping, + gameMode, + display + ); + + FS_ClientboundPlayerInfoUpdatePacket packet = FancySitula.PACKET_FACTORY.createPlayerInfoUpdatePacket( + EnumSet.of(FS_ClientboundPlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME), + List.of(packetEntry) + ); + + packet.send(new FS_RealPlayer(viewer)); + } + + private void removeCustomEntry(Player viewer, CustomEntryState state) { + FS_ClientboundPlayerInfoRemovePacket packet = FancySitula.PACKET_FACTORY.createPlayerInfoRemovePacket(List.of(state.uuid)); + packet.send(new FS_RealPlayer(viewer)); + teamManager.removeEntity(viewer, state.profileName); + } + + private boolean skinsMatch(CustomEntryState state, CustomTablistEntry entry) { + if (state.skin == null && entry.skin() == null) { + return true; + } + if (state.skin == null || entry.skin() == null) { + return false; + } + return state.skin.equals(entry.skin()); + } + + private static class HeaderFooterState { + private final Component header; + private final Component footer; + + private HeaderFooterState(Component header, Component footer) { + this.header = header; + this.footer = footer; + } + } + + private static class CustomEntryState { + private final UUID uuid; + private final String profileName; + private Component display; + private int sortPriority; + private final TablistSkin skin; + + private CustomEntryState(UUID uuid, String profileName, Component display, int sortPriority, TablistSkin skin) { + this.uuid = uuid; + this.profileName = profileName; + this.display = display; + this.sortPriority = sortPriority; + this.skin = skin; + } + } +} diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/tablist/TablistRepository.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/tablist/TablistRepository.java new file mode 100644 index 00000000..1c51df6e --- /dev/null +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/tablist/TablistRepository.java @@ -0,0 +1,32 @@ +package de.oliver.fancyvisuals.tablist; + +import de.oliver.fancyvisuals.tablist.data.CustomTablistEntries; +import de.oliver.fancyvisuals.tablist.data.TablistHeaderFooter; +import de.oliver.fancyvisuals.tablist.data.TablistPlayerFormat; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public interface TablistRepository { + + TablistHeaderFooter DEFAULT_HEADER_FOOTER = new TablistHeaderFooter( + "FancyVisuals", + "Online: %server_online%" + ); + + TablistPlayerFormat DEFAULT_PLAYER_FORMAT = new TablistPlayerFormat( + "", + "%player_name%", + "", + 100 + ); + + CustomTablistEntries DEFAULT_CUSTOM_ENTRIES = new CustomTablistEntries(List.of()); + + @NotNull TablistHeaderFooter getHeaderFooterForPlayer(@NotNull Player player); + + @NotNull TablistPlayerFormat getPlayerFormatForPlayer(@NotNull Player player); + + @NotNull CustomTablistEntries getCustomEntriesForPlayer(@NotNull Player player); +} diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/tablist/TablistTeamManager.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/tablist/TablistTeamManager.java new file mode 100644 index 00000000..2e57b38e --- /dev/null +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/tablist/TablistTeamManager.java @@ -0,0 +1,118 @@ +package de.oliver.fancyvisuals.tablist; + +import de.oliver.fancysitula.api.entities.FS_RealPlayer; +import de.oliver.fancysitula.api.packets.FS_Color; +import de.oliver.fancysitula.api.teams.FS_CollisionRule; +import de.oliver.fancysitula.api.teams.FS_NameTagVisibility; +import de.oliver.fancysitula.api.teams.FS_Team; +import de.oliver.fancysitula.factories.FancySitula; +import de.oliver.fancyvisuals.FancyVisuals; +import net.kyori.adventure.text.Component; +import org.bukkit.entity.Player; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +public class TablistTeamManager { + + private final Map> viewerAssignments; + private final Map> viewerTeams; + + public TablistTeamManager() { + this.viewerAssignments = new HashMap<>(); + this.viewerTeams = new HashMap<>(); + } + + public void assignEntity(Player viewer, String entityName, int sortPriority) { + if (viewer == null || !viewer.isOnline()) { + return; + } + + String teamName = getTeamName(sortPriority); + Map assignments = viewerAssignments.computeIfAbsent(viewer.getUniqueId(), key -> new HashMap<>()); + String currentTeam = assignments.get(entityName); + if (teamName.equals(currentTeam)) { + return; + } + + FS_RealPlayer fsViewer = new FS_RealPlayer(viewer); + + if (currentTeam != null) { + FS_Team oldTeam = buildTeam(currentTeam); + FancySitula.TEAM_FACTORY.removeEntitiesFromTeamFor(fsViewer, oldTeam, List.of(entityName)); + } + + ensureTeam(viewer, teamName); + + FS_Team team = buildTeam(teamName); + FancySitula.TEAM_FACTORY.addEntitiesToTeamFor(fsViewer, team, List.of(entityName)); + assignments.put(entityName, teamName); + } + + public void removeEntity(Player viewer, String entityName) { + if (viewer == null || !viewer.isOnline()) { + return; + } + + Map assignments = viewerAssignments.get(viewer.getUniqueId()); + if (assignments == null) { + return; + } + + String teamName = assignments.remove(entityName); + if (teamName == null) { + return; + } + + FS_Team team = buildTeam(teamName); + FS_RealPlayer fsViewer = new FS_RealPlayer(viewer); + FancySitula.TEAM_FACTORY.removeEntitiesFromTeamFor(fsViewer, team, List.of(entityName)); + } + + public void removeViewer(Player viewer) { + if (viewer == null) { + return; + } + + viewerAssignments.remove(viewer.getUniqueId()); + viewerTeams.remove(viewer.getUniqueId()); + } + + private void ensureTeam(Player viewer, String teamName) { + Set created = viewerTeams.computeIfAbsent(viewer.getUniqueId(), key -> new HashSet<>()); + if (created.contains(teamName)) { + return; + } + + FS_Team team = buildTeam(teamName); + FS_RealPlayer fsViewer = new FS_RealPlayer(viewer); + FancySitula.TEAM_FACTORY.createTeamFor(fsViewer, team); + created.add(teamName); + } + + private FS_Team buildTeam(String teamName) { + boolean hideNametag = FancyVisuals.get().getNametagConfig().hideVanillaNametag(); + FS_NameTagVisibility visibility = hideNametag ? FS_NameTagVisibility.NEVER : FS_NameTagVisibility.ALWAYS; + return new FS_Team( + teamName, + Component.text(teamName), + true, + true, + visibility, + FS_CollisionRule.ALWAYS, + FS_Color.WHITE, + Component.empty(), + Component.empty(), + List.of() + ); + } + + private String getTeamName(int sortPriority) { + int normalized = Math.max(0, Math.min(999, sortPriority)); + return "fv" + String.format("%03d", normalized); + } +} diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/tablist/TablistUtils.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/tablist/TablistUtils.java new file mode 100644 index 00000000..942b354f --- /dev/null +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/tablist/TablistUtils.java @@ -0,0 +1,78 @@ +package de.oliver.fancyvisuals.tablist; + +import de.oliver.fancysitula.api.utils.FS_GameProfile; +import de.oliver.fancysitula.api.utils.FS_GameType; +import de.oliver.fancyvisuals.tablist.data.CustomTablistEntry; +import org.bukkit.GameMode; +import org.bukkit.entity.Player; + +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +public final class TablistUtils { + + private TablistUtils() { + } + + public static FS_GameProfile toProfile(Player player) { + return FS_GameProfile.fromBukkit(player.getPlayerProfile()); + } + + public static FS_GameType toGameType(GameMode gameMode) { + if (gameMode == null) { + return FS_GameType.SURVIVAL; + } + + return switch (gameMode) { + case CREATIVE -> FS_GameType.CREATIVE; + case ADVENTURE -> FS_GameType.ADVENTURE; + case SPECTATOR -> FS_GameType.SPECTATOR; + case SURVIVAL -> FS_GameType.SURVIVAL; + }; + } + + public static FS_GameType toGameType(String name) { + if (name == null) { + return FS_GameType.SURVIVAL; + } + + try { + return FS_GameType.valueOf(name.toUpperCase()); + } catch (IllegalArgumentException e) { + return FS_GameType.SURVIVAL; + } + } + + public static UUID buildEntryUuid(Player viewer, CustomTablistEntry entry) { + String key = viewer.getUniqueId() + ":" + entry.id(); + return UUID.nameUUIDFromBytes(key.getBytes(StandardCharsets.UTF_8)); + } + + public static String buildProfileName(CustomTablistEntry entry) { + String base = entry.profileName(); + String fallback = "fv" + shortHash(entry.id()); + + if (base == null || base.isBlank()) { + base = fallback; + } + + String sanitized = base.replaceAll("[^A-Za-z0-9_]", ""); + if (sanitized.isEmpty()) { + sanitized = fallback; + } + + if (sanitized.length() > 16) { + sanitized = sanitized.substring(0, 16); + } + + return sanitized; + } + + private static String shortHash(String value) { + String hex = Integer.toHexString(value.hashCode()); + if (hex.length() > 10) { + return hex.substring(0, 10); + } + return hex; + } +} diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/tablist/data/CustomTablistEntries.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/tablist/data/CustomTablistEntries.java new file mode 100644 index 00000000..9766d88f --- /dev/null +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/tablist/data/CustomTablistEntries.java @@ -0,0 +1,12 @@ +package de.oliver.fancyvisuals.tablist.data; + +import com.google.gson.annotations.SerializedName; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public record CustomTablistEntries( + @SerializedName("entries") + @NotNull List entries +) { +} diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/tablist/data/CustomTablistEntry.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/tablist/data/CustomTablistEntry.java new file mode 100644 index 00000000..6f3e2b36 --- /dev/null +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/tablist/data/CustomTablistEntry.java @@ -0,0 +1,23 @@ +package de.oliver.fancyvisuals.tablist.data; + +import com.google.gson.annotations.SerializedName; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public record CustomTablistEntry( + @SerializedName("id") + @NotNull String id, + @SerializedName("text") + @NotNull String text, + @SerializedName("sort_priority") + int sortPriority, + @SerializedName("ping") + int ping, + @SerializedName("game_mode") + @Nullable String gameMode, + @SerializedName("profile_name") + @Nullable String profileName, + @SerializedName("skin") + @Nullable TablistSkin skin +) { +} diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/tablist/data/TablistHeaderFooter.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/tablist/data/TablistHeaderFooter.java new file mode 100644 index 00000000..596239b4 --- /dev/null +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/tablist/data/TablistHeaderFooter.java @@ -0,0 +1,12 @@ +package de.oliver.fancyvisuals.tablist.data; + +import com.google.gson.annotations.SerializedName; +import org.jetbrains.annotations.NotNull; + +public record TablistHeaderFooter( + @SerializedName("header") + @NotNull String header, + @SerializedName("footer") + @NotNull String footer +) { +} diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/tablist/data/TablistPlayerFormat.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/tablist/data/TablistPlayerFormat.java new file mode 100644 index 00000000..fb4d2dde --- /dev/null +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/tablist/data/TablistPlayerFormat.java @@ -0,0 +1,16 @@ +package de.oliver.fancyvisuals.tablist.data; + +import com.google.gson.annotations.SerializedName; +import org.jetbrains.annotations.NotNull; + +public record TablistPlayerFormat( + @SerializedName("prefix") + @NotNull String prefix, + @SerializedName("name") + @NotNull String name, + @SerializedName("suffix") + @NotNull String suffix, + @SerializedName("sort_priority") + int sortPriority +) { +} diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/tablist/data/TablistSkin.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/tablist/data/TablistSkin.java new file mode 100644 index 00000000..ccdc9748 --- /dev/null +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/tablist/data/TablistSkin.java @@ -0,0 +1,48 @@ +package de.oliver.fancyvisuals.tablist.data; + +import com.google.gson.annotations.SerializedName; +import de.oliver.fancysitula.api.utils.FS_GameProfile; +import org.jetbrains.annotations.Nullable; + +public record TablistSkin( + @SerializedName("texture_value") + @Nullable String textureValue, + @SerializedName("texture_signature") + @Nullable String textureSignature, + @SerializedName("skin_texture_asset") + @Nullable String skinTextureAsset, + @SerializedName("cape_texture_asset") + @Nullable String capeTextureAsset, + @SerializedName("elytra_texture_asset") + @Nullable String elytraTextureAsset, + @SerializedName("model") + @Nullable Model model +) { + + public void applyTo(FS_GameProfile profile) { + if (textureValue != null && textureSignature != null) { + profile.getProperties().put("textures", new FS_GameProfile.Property("textures", textureValue, textureSignature)); + } + + if (skinTextureAsset != null) { + profile.setSkinTextureAsset(skinTextureAsset); + } + if (capeTextureAsset != null) { + profile.setCapeTextureAsset(capeTextureAsset); + } + if (elytraTextureAsset != null) { + profile.setElytraTextureAsset(elytraTextureAsset); + } + + if (model != null) { + profile.setModelType(model == Model.SLIM ? "SLIM" : "DEFAULT"); + } + } + + public enum Model { + @SerializedName("default") + DEFAULT, + @SerializedName("slim") + SLIM + } +} diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/titles/JsonTitleRepository.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/titles/JsonTitleRepository.java new file mode 100644 index 00000000..66014d54 --- /dev/null +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/titles/JsonTitleRepository.java @@ -0,0 +1,44 @@ +package de.oliver.fancyvisuals.titles; + +import de.oliver.fancyvisuals.api.Context; +import de.oliver.fancyvisuals.utils.ContextLookup; +import de.oliver.fancyvisuals.utils.JsonContextStore; +import de.oliver.jdb.JDB; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class JsonTitleRepository implements TitleRepository { + + private static final String BASE_PATH = "plugins/FancyVisuals/data/titles/"; + + private final Map> stores; + + public JsonTitleRepository() { + stores = new ConcurrentHashMap<>(); + JDB jdb = new JDB(BASE_PATH); + + for (Context ctx : Context.values()) { + stores.put(ctx, new JsonContextStore<>(jdb, ctx, TitleConfig.class)); + } + + initialConfig(); + } + + @Override + public @NotNull TitleConfig getTitlesForPlayer(@NotNull Player player) { + return ContextLookup.resolve(player, (ctx, id) -> stores.get(ctx).get(id), DEFAULT_TITLES); + } + + private void initialConfig() { + File baseDir = new File(BASE_PATH); + if (baseDir.exists()) { + return; + } + + stores.get(Context.SERVER).set("global", DEFAULT_TITLES); + } +} diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/titles/TitleConfig.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/titles/TitleConfig.java new file mode 100644 index 00000000..e10a0001 --- /dev/null +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/titles/TitleConfig.java @@ -0,0 +1,14 @@ +package de.oliver.fancyvisuals.titles; + +import com.google.gson.annotations.SerializedName; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public record TitleConfig( + @SerializedName("interval_ms") + int intervalMs, + @SerializedName("messages") + @NotNull List messages +) { +} diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/titles/TitleListeners.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/titles/TitleListeners.java new file mode 100644 index 00000000..1edd7cf1 --- /dev/null +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/titles/TitleListeners.java @@ -0,0 +1,42 @@ +package de.oliver.fancyvisuals.titles; + +import de.oliver.fancyvisuals.FancyVisuals; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerChangedWorldEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; + +public class TitleListeners implements Listener { + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + if (!FancyVisuals.get().getFancyVisualsConfig().isTitlesEnabled()) { + return; + } + + Player player = event.getPlayer(); + FancyVisuals.get().getTitleManager().handleJoin(player); + } + + @EventHandler + public void onPlayerQuit(PlayerQuitEvent event) { + if (!FancyVisuals.get().getFancyVisualsConfig().isTitlesEnabled()) { + return; + } + + Player player = event.getPlayer(); + FancyVisuals.get().getTitleManager().handleQuit(player); + } + + @EventHandler + public void onPlayerWorldChange(PlayerChangedWorldEvent event) { + if (!FancyVisuals.get().getFancyVisualsConfig().isTitlesEnabled()) { + return; + } + + Player player = event.getPlayer(); + FancyVisuals.get().getTitleManager().handleContextChange(player); + } +} diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/titles/TitleManager.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/titles/TitleManager.java new file mode 100644 index 00000000..591e6423 --- /dev/null +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/titles/TitleManager.java @@ -0,0 +1,122 @@ +package de.oliver.fancyvisuals.titles; + +import de.oliver.fancyvisuals.FancyVisuals; +import de.oliver.fancyvisuals.utils.TextRenderer; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.title.Title; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public class TitleManager { + + private final FancyVisuals plugin; + private final TitleRepository repository; + private final ScheduledExecutorService scheduler; + private final Map states; + + public TitleManager(FancyVisuals plugin, TitleRepository repository) { + this.plugin = plugin; + this.repository = repository; + this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> new Thread(r, "TitleScheduler")); + this.states = new ConcurrentHashMap<>(); + } + + public void init() { + int intervalMs = plugin.getFancyVisualsConfig().getTitlesUpdateIntervalMs(); + scheduler.scheduleWithFixedDelay(this::queueUpdateCycle, intervalMs, intervalMs, TimeUnit.MILLISECONDS); + } + + public void shutdown() { + scheduler.shutdownNow(); + } + + public void clearAll() { + states.clear(); + } + + public void handleJoin(Player player) { + if (!plugin.getFancyVisualsConfig().isTitlesEnabled()) { + return; + } + + updateForPlayer(player); + } + + public void handleQuit(Player player) { + states.remove(player.getUniqueId()); + } + + public void handleContextChange(Player player) { + if (!plugin.getFancyVisualsConfig().isTitlesEnabled()) { + return; + } + + updateForPlayer(player); + } + + private void queueUpdateCycle() { + Bukkit.getScheduler().runTask(plugin, this::runUpdateCycle); + } + + private void runUpdateCycle() { + if (!plugin.getFancyVisualsConfig().isTitlesEnabled()) { + return; + } + + for (Player player : Bukkit.getOnlinePlayers()) { + updateForPlayer(player); + } + } + + private void updateForPlayer(Player player) { + TitleConfig config = repository.getTitlesForPlayer(player); + List messages = config.messages(); + if (messages.isEmpty()) { + states.remove(player.getUniqueId()); + return; + } + + int intervalMs = config.intervalMs() > 0 ? config.intervalMs() : plugin.getFancyVisualsConfig().getTitlesUpdateIntervalMs(); + TitleState state = states.computeIfAbsent(player.getUniqueId(), key -> new TitleState()); + + long now = System.currentTimeMillis(); + if (now - state.lastSentAt < intervalMs) { + return; + } + + int index = state.index % messages.size(); + TitleMessage message = messages.get(index); + + Component title = TextRenderer.render(message.title(), player); + Component subtitle = TextRenderer.render(message.subtitle(), player); + + Title.Times times = Title.Times.times( + ticksToDuration(message.fadeInTicks()), + ticksToDuration(message.stayTicks()), + ticksToDuration(message.fadeOutTicks()) + ); + + player.showTitle(Title.title(title, subtitle, times)); + + state.index = (state.index + 1) % messages.size(); + state.lastSentAt = now; + } + + private Duration ticksToDuration(int ticks) { + return Duration.ofMillis(Math.max(0, ticks) * 50L); + } + + private static class TitleState { + private long lastSentAt; + private int index; + } +} diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/titles/TitleMessage.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/titles/TitleMessage.java new file mode 100644 index 00000000..2f6d60ff --- /dev/null +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/titles/TitleMessage.java @@ -0,0 +1,18 @@ +package de.oliver.fancyvisuals.titles; + +import com.google.gson.annotations.SerializedName; +import org.jetbrains.annotations.NotNull; + +public record TitleMessage( + @SerializedName("title") + @NotNull String title, + @SerializedName("subtitle") + @NotNull String subtitle, + @SerializedName("fade_in_ticks") + int fadeInTicks, + @SerializedName("stay_ticks") + int stayTicks, + @SerializedName("fade_out_ticks") + int fadeOutTicks +) { +} diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/titles/TitleRepository.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/titles/TitleRepository.java new file mode 100644 index 00000000..028dd145 --- /dev/null +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/titles/TitleRepository.java @@ -0,0 +1,24 @@ +package de.oliver.fancyvisuals.titles; + +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public interface TitleRepository { + + TitleConfig DEFAULT_TITLES = new TitleConfig( + 5000, + List.of( + new TitleMessage( + "FancyVisuals", + "Welcome, %player_name%", + 10, + 40, + 10 + ) + ) + ); + + @NotNull TitleConfig getTitlesForPlayer(@NotNull Player player); +} diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/utils/ContextLookup.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/utils/ContextLookup.java new file mode 100644 index 00000000..4bec7184 --- /dev/null +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/utils/ContextLookup.java @@ -0,0 +1,45 @@ +package de.oliver.fancyvisuals.utils; + +import de.oliver.fancyvisuals.api.Context; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.function.BiFunction; + +public final class ContextLookup { + + private ContextLookup() { + } + + public static @Nullable T resolve(@NotNull Player player, + @NotNull BiFunction lookup, + @Nullable T fallback) { + T value = lookup.apply(Context.PLAYER, player.getUniqueId().toString()); + if (value != null) { + return value; + } + + if (VaultHelper.isVaultLoaded() && VaultHelper.getPermission() != null) { + String group = VaultHelper.getPermission().getPrimaryGroup(player); + if (group != null && !group.isEmpty()) { + value = lookup.apply(Context.GROUP, group); + if (value != null) { + return value; + } + } + } + + value = lookup.apply(Context.WORLD, player.getWorld().getName()); + if (value != null) { + return value; + } + + value = lookup.apply(Context.SERVER, "global"); + if (value != null) { + return value; + } + + return fallback; + } +} diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/utils/JsonContextStore.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/utils/JsonContextStore.java new file mode 100644 index 00000000..c3c5f979 --- /dev/null +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/utils/JsonContextStore.java @@ -0,0 +1,55 @@ +package de.oliver.fancyvisuals.utils; + +import de.oliver.fancyanalytics.logger.properties.ThrowableProperty; +import de.oliver.fancyvisuals.FancyVisuals; +import de.oliver.fancyvisuals.api.Context; +import de.oliver.jdb.JDB; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class JsonContextStore { + + private final JDB jdb; + private final Context context; + private final Class type; + + public JsonContextStore(JDB jdb, Context context, Class type) { + this.jdb = jdb; + this.context = context; + this.type = type; + } + + public void set(@NotNull String id, @NotNull T value) { + try { + jdb.set(context.getName() + "/" + id, value); + } catch (IOException e) { + FancyVisuals.getFancyLogger().error("Failed to save context data for id " + id, ThrowableProperty.of(e)); + } + } + + public @Nullable T get(@NotNull String id) { + try { + return jdb.get(context.getName() + "/" + id, type); + } catch (IOException e) { + FancyVisuals.getFancyLogger().error("Failed to load context data for id " + id, ThrowableProperty.of(e)); + return null; + } + } + + public void remove(@NotNull String id) { + jdb.delete(context.getName() + "/" + id); + } + + public @NotNull List getAll() { + try { + return jdb.getAll(context.getName(), type); + } catch (IOException e) { + FancyVisuals.getFancyLogger().error("Failed to load context data list", ThrowableProperty.of(e)); + return new ArrayList<>(); + } + } +} diff --git a/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/utils/TextRenderer.java b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/utils/TextRenderer.java new file mode 100644 index 00000000..f3b528cc --- /dev/null +++ b/plugins/fancyvisuals/src/main/java/de/oliver/fancyvisuals/utils/TextRenderer.java @@ -0,0 +1,39 @@ +package de.oliver.fancyvisuals.utils; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.Nullable; +import org.lushplugins.chatcolorhandler.ModernChatColorHandler; + +import java.util.List; + +public final class TextRenderer { + + private static final MiniMessage MINI_MESSAGE = MiniMessage.miniMessage(); + + private TextRenderer() { + } + + public static Component render(@Nullable String text, @Nullable Player player) { + if (text == null || text.isEmpty()) { + return Component.empty(); + } + + if (Bukkit.isStopping()) { + return MINI_MESSAGE.deserialize(text); + } + + return ModernChatColorHandler.translate(text, player); + } + + public static Component renderLines(@Nullable List lines, @Nullable Player player) { + if (lines == null || lines.isEmpty()) { + return Component.empty(); + } + + String joined = String.join("\n", lines); + return render(joined, player); + } +}