MegavexNetwork / scoreboard-library

Powerful packet-level scoreboard library for Paper/Spigot servers
https://megavex.net
MIT License
150 stars 13 forks source link

Per player dynamic sidebar #42

Closed iimrudy closed 5 months ago

iimrudy commented 5 months ago

Hello how can i build a scoreboard like the one in the example with self player data like adding to example the player's HEALTH, or maybe adding rank etc..

thanks...

public class ExampleComponentSidebar {
  private final Sidebar sidebar;
  private final ComponentSidebarLayout componentSidebar;
  private final SidebarAnimation<Component> titleAnimation;

  public ExampleComponentSidebar(@NotNull Plugin plugin, @NotNull Sidebar sidebar) {
    this.sidebar = sidebar;

    this.titleAnimation = createGradientAnimation(Component.text("Sidebar Example", Style.style(TextDecoration.BOLD)));
    var title = SidebarComponent.animatedLine(titleAnimation);

    SimpleDateFormat dtf = new SimpleDateFormat("HH:mm:ss");

    // Custom SidebarComponent, see below for how an implementation might look like
    SidebarComponent onlinePlayers = new KeyValueSidebarComponent(
      Component.text("Online players"),
      () -> Component.text(plugin.getServer().getOnlinePlayers().size())
    );

    SidebarComponent lines = SidebarComponent.builder()
      .addDynamicLine(() -> {
        var time = dtf.format(new Date());
        return Component.text(time, NamedTextColor.GRAY);
      })
      .addBlankLine()
      .addStaticLine(Component.text("A static line"))
      .addComponent(onlinePlayers)
      .addBlankLine()
      .addStaticLine(Component.text("epicserver.net", NamedTextColor.AQUA))
      .addBlankLine()
// PLAYER HEALTH HERE
      .build();

    this.componentSidebar = new ComponentSidebarLayout(title, lines);
  }

  // Called every tick
  public void tick() {
    // Advance title animation to the next frame
    titleAnimation.nextFrame();

    // Update sidebar title & lines
    componentSidebar.apply(sidebar);
  }

  private @NotNull SidebarAnimation<Component> createGradientAnimation(@NotNull Component text) {
    float step = 1f / 8f;

    TagResolver.Single textPlaceholder = Placeholder.component("text", text);
    List<Component> frames = new ArrayList<>((int) (2f / step));

    float phase = -1f;
    while (phase < 1) {
      frames.add(MiniMessage.miniMessage().deserialize("<gradient:yellow:gold:" + phase + "><text>", textPlaceholder));
      phase += step;
    }

    return new CollectionSidebarAnimation<>(frames);
  }
}
vytskalt commented 5 months ago

You're currently supposed to create a different Sidebar instance for every player. I plan on making this easier in the next major version

iimrudy commented 5 months ago

Alright tysm

On Tue, May 21, 2024, 7:03 AM vytskalt @.***> wrote:

You're currently supposed to create a different Sidebar instance for every player. I plan on making this easier in the next major version

— Reply to this email directly, view it on GitHub https://github.com/MegavexNetwork/scoreboard-library/issues/42#issuecomment-2121745677, or unsubscribe https://github.com/notifications/unsubscribe-auth/ANA27SBM55RZS6YUWBHLDLLZDLIRHAVCNFSM6AAAAABIALXEM6VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDCMRRG42DKNRXG4 . You are receiving this because you authored the thread.Message ID: @.***>

iimrudy commented 5 months ago

Creating a scoreboard for each player is going to impact a lot on performance or ram memory?

i'm trying to keep resources as low as possible :)

vytskalt commented 5 months ago

Server performance wouldnt change because most work is being done off the main thread.

Memory I guess would be larger than a single sidebar, but most likely nowhere near where it would actually matter. You can create the sidebar like this to make the situation better: .createSidebar(Sidebar.MAX_LINES, Locale.US);

Would be interesting if you profiled it though

iimrudy commented 5 months ago

i'am going to try that, i'll post profilings asap

thanks for the help

iimrudy commented 5 months ago

Hello sorry to bother again,

i wrote a bunch of code where the personal scoreboard seems to work fine but the animated title it is not working anymore and i can't find why, may i ask you for a help?

PerPlayerSidebar.java

public class PerPlayerSidebar implements Listener, Runnable {

    private final Map<Player, Tuple<Sidebar, ComponentSidebarLayout>> playersSidebarMap = new ConcurrentHashMap<>();
    private final ScoreboardLibrary scoreboardLibrary;
    private final BukkitTask task;

    //@Getter
    //@Setter
    private PlayerSidebarComponentGenerator generator;

    public PerPlayerSidebar(Plugin plugin, ScoreboardLibrary library, PlayerSidebarComponentGenerator generator) {
        this.scoreboardLibrary = library;
        this.generator = generator;
        plugin.getServer().getPluginManager().registerEvents(this, plugin);
        plugin.getServer().getOnlinePlayers().forEach(this::addPlayer);
        this.task = plugin.getServer().getScheduler().runTaskTimer(plugin, this, 0, 1L);
    }

    @Override
    public void run() {
        for(var entry : playersSidebarMap.entrySet()) {
            // per player apply
            var tuple = entry.getValue();
            tuple.getRight().apply(tuple.getLeft());
        }
    }

    @EventHandler
    private void onPlayerJoin(PlayerJoinEvent event) {
        this.addPlayer(event.getPlayer());
    }

    private void addPlayer(Player p) {
        var sidebar = this.scoreboardLibrary.createSidebar();
        sidebar.addPlayer(p);
        this.playersSidebarMap.put(p, new Tuple<>(sidebar, generator.createSidebarComponent(p)));
        task.getOwner().getLogger().info("init scoreboard for" + p.getName());
    }

    @EventHandler
    private void onPlayerQuitEvent(PlayerQuitEvent event) {
        var tuple = this.playersSidebarMap.get(event.getPlayer());
        if (!Objects.isNull(tuple)) {
            tuple.getLeft().removePlayer(event.getPlayer());
            this.playersSidebarMap.remove(event.getPlayer());
        }
        task.getOwner().getLogger().info("deinit scoreboard for" + event.getPlayer().getName());

    }

    public void destroy() {
        this.task.cancel();
        for(var entry : playersSidebarMap.entrySet()) {
            var player = entry.getKey();
            var tuple = entry.getValue();
            tuple.getLeft().removePlayer(player);
        }
        this.playersSidebarMap.clear();
    }
}

PlayerSidebarComponentGenerator.java

import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
import net.megavex.scoreboardlibrary.api.sidebar.component.ComponentSidebarLayout;
import net.megavex.scoreboardlibrary.api.sidebar.component.animation.CollectionSidebarAnimation;
import net.megavex.scoreboardlibrary.api.sidebar.component.animation.SidebarAnimation;
import org.bukkit.entity.Player;

import java.util.ArrayList;
import java.util.List;

@FunctionalInterface
public interface PlayerSidebarComponentGenerator {

    ComponentSidebarLayout createSidebarComponent(Player player);

    default SidebarAnimation<Component> createGradientAnimation(String startColor, String endColor, Component text) {
        float step = 1f / 8f;

        TagResolver.Single textPlaceholder = Placeholder.component("text", text);
        List<Component> frames = new ArrayList<>((int) (2f / step));

        float phase = -1f;
        while (phase < 1) {
            frames.add(MiniMessage.miniMessage().deserialize("<gradient:" + startColor + ":" + endColor + ":" + phase + "><text>", textPlaceholder));
            phase += step;
        }

        return new CollectionSidebarAnimation<>(frames);
    }
}

LobbyScoreboardV2

public class LobbyScoreboardV2 implements PlayerSidebarComponentGenerator {

    private static final PaperRanks ranks = new PaperRanks();

    @Override
    public ComponentSidebarLayout createSidebarComponent(Player player) {
        // per player animation otherwise there will be frame skipping
        var titleAnimation = createGradientAnimation("#23E7C6", "#690AC7", Component.text("TESTTTTT", Style.style(TextDecoration.BOLD)));
        var title = SidebarComponent.animatedLine(titleAnimation);

        // Custom SidebarComponent, see below for how an implementation might look like
        SidebarComponent onlinePlayers = new KeyValueSidebarComponent(
                Component.text("Online players"),
                () -> Component.text(Bukkit.getServer().getOnlinePlayers().size())
        );

        //var serverName = new KeyValueSidebarComponent(Component.text("SERVER"), () -> Component.text(Core.getInstance().getServerName()));
        //var serverCategory = new KeyValueSidebarComponent(Component.text("CAT"), () -> Component.text(Core.getInstance().getServerCategory().name()));

        SidebarComponent lines = SidebarComponent.builder()
                .addDynamicLine(() -> {
                    var rank = ranks.getRank(player);
                    return Component.text(rank.getPrefix(), NamedTextColor.GRAY);
                })
                .addDynamicLine(() -> {
                    var ping = player.getPing();
                    return Component.text("PING: " + ping);
                })
                .addBlankLine()
                .addStaticLine(Component.text("TEST-server"))
                .addStaticLine(Component.text("TEST"))
                .addBlankLine()
                .addComponent(onlinePlayers)
                .addBlankLine()
                .addStaticLine(Component.text("TEst", NamedTextColor.AQUA))
                .build();

        return new ComponentSidebarLayout(title, lines);
    }
}

Tuple.java

import java.util.Objects;

public class Tuple <A, B> {

    private final A left;
    private final B right;

    public Tuple(A a, B b) {
        this.left = a;
        this.right = b;
    }

    public A getLeft() {
        return left;
    }

    public B getRight() {
        return right;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Tuple<?, ?> tuple = (Tuple<?, ?>) o;
        return Objects.equals(left, tuple.left) && Objects.equals(right, tuple.right);
    }

    @Override
    public int hashCode() {
        return Objects.hash(left, right);
    }
}

to initialize this add to the onEnable of the plugin

new PerPlayerSidebar(this, getScoreboardLibrary(), new LobbyScoreboardV2());
vytskalt commented 5 months ago

You need to be advancing the title animation by calling titleAnimation.nextFrame(); every tick or so

iimrudy commented 5 months ago

oh that make sense tysm, i tought it was automatic 😳

iimrudy commented 5 months ago

Hello i've made some tests with 100 players online (bots), the scoreboard ticker is async so no performance impact overall, very low memory usage (less then 700kb)

the only things that has a little bit of impact is the ScoreboardLibrary.createSidebar() but's has no relevance overall...

ill leave the spark reports

memory profiler

server was having 1gb of ram, using aikar flags, paper version: git-Paper-496 (MC: 1.20.4) (Implementing API version 1.20.4-R0.1-SNAPSHOT) (Git: 7ac24a1 on ver/1.20.4)

for my use-case i'm never going to have an paper instance with more then 100 players so i'm very happy with that results :)

vytskalt commented 5 months ago

That's great to hear