Compare commits

..

No commits in common. "prod" and "1.3" have entirely different histories.
prod ... 1.3

7 changed files with 5 additions and 410 deletions

View File

@ -6,7 +6,7 @@
<groupId>ch.bensuisse</groupId>
<artifactId>MiningMan</artifactId>
<version>2.0</version>
<version>1.3</version>
<packaging>jar</packaging>
<name>MiningMan</name>

View File

@ -5,8 +5,6 @@ import ch.bensuisse.miningman.features.announce.AnnounceCommand;
import ch.bensuisse.miningman.features.chat.ChatListener;
import ch.bensuisse.miningman.features.chat.ClearChatCommand;
import ch.bensuisse.miningman.features.general.ConnectionListener;
import ch.bensuisse.miningman.features.scoreboard.ScoreboardCommand;
import ch.bensuisse.miningman.features.scoreboard.ScoreboardService;
import ch.bensuisse.miningman.features.tablist.TablistService;
import ch.bensuisse.miningman.managers.JoinManager;
import net.luckperms.api.LuckPerms;
@ -18,7 +16,6 @@ public final class MiningMan extends JavaPlugin {
private LuckPerms luckPerms;
private TablistService tablistService;
private ScoreboardService scoreboardService;
@Override
public void onEnable() {
@ -29,7 +26,6 @@ public final class MiningMan extends JavaPlugin {
}
this.tablistService = new TablistService(this, luckPerms);
this.scoreboardService = new ScoreboardService(this, luckPerms);
registerListeners();
registerCommands();
@ -42,8 +38,6 @@ public final class MiningMan extends JavaPlugin {
public void onDisable() {
if (tablistService != null)
tablistService.onDisable();
if (scoreboardService != null)
scoreboardService.onDisable();
}
private boolean setupLuckPerms() {
@ -61,7 +55,6 @@ public final class MiningMan extends JavaPlugin {
pm.registerEvents(new ConnectionListener(luckPerms), this);
pm.registerEvents(new ChatListener(this), this);
pm.registerEvents(this.tablistService, this);
pm.registerEvents(this.scoreboardService, this);
}
private void registerCommands() {
@ -78,17 +71,9 @@ public final class MiningMan extends JavaPlugin {
ClearChatCommand clearChatCommand = new ClearChatCommand();
getCommand("clearchat").setExecutor(clearChatCommand);
getCommand("clearchat").setTabCompleter(clearChatCommand);
ScoreboardCommand scoreboardCommand = new ScoreboardCommand(scoreboardService);
getCommand("sb").setExecutor(scoreboardCommand);
getCommand("sb").setTabCompleter(scoreboardCommand);
}
public TablistService getTablistService() {
return tablistService;
}
public ScoreboardService getScoreboardService() {
return scoreboardService;
}
}

View File

@ -5,7 +5,6 @@ import net.luckperms.api.LuckPerms;
import net.luckperms.api.model.user.User;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
@ -18,14 +17,14 @@ public class ConnectionListener implements Listener {
this.luckPerms = luckPerms;
}
@EventHandler(priority = EventPriority.HIGHEST)
@EventHandler
public void onJoin(PlayerJoinEvent event) {
String prefix = getPrefix(event.getPlayer());
String msg = "<dark_gray>[<green>+<dark_gray>] <reset>" + prefix + " <white>" + event.getPlayer().getName();
event.joinMessage(MiniMessage.miniMessage().deserialize(msg));
}
@EventHandler(priority = EventPriority.HIGHEST)
@EventHandler
public void onQuit(PlayerQuitEvent event) {
String prefix = getPrefix(event.getPlayer());
String msg = "<dark_gray>[<red>-<dark_gray>] <reset>" + prefix + " <white>" + event.getPlayer().getName();

View File

@ -1,51 +0,0 @@
package ch.bensuisse.miningman.features.scoreboard;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import org.bukkit.Sound;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.Collections;
import java.util.List;
public class ScoreboardCommand implements CommandExecutor, TabCompleter {
private static final TextColor THEME_COLOR = TextColor.fromHexString("#FFA80F");
private final ScoreboardService scoreboardService;
public ScoreboardCommand(ScoreboardService scoreboardService) {
this.scoreboardService = scoreboardService;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage(Component.text("ᴏɴʟʏ ᴘʟᴀʏᴇʀs ᴄᴀɴ ᴜsᴇ ᴛʜɪs ᴄᴏᴍᴍᴀɴᴅ.", NamedTextColor.RED));
return true;
}
boolean enabled = scoreboardService.toggle(player);
String state = enabled ? "ᴇɴᴀʙʟᴇᴅ" : "ᴅɪsᴀʙʟᴇᴅ";
player.sendMessage(Component.text("➤ ", NamedTextColor.WHITE)
.append(Component.text("sᴄᴏʀᴇʙᴏᴀʀᴅ ", THEME_COLOR))
.append(Component.text(state, THEME_COLOR)));
player.playSound(player.getLocation(), Sound.UI_BUTTON_CLICK, 1.0f, enabled ? 1.2f : 0.8f);
return true;
}
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias,
@NotNull String[] args) {
return Collections.emptyList();
}
}

View File

@ -1,337 +0,0 @@
package ch.bensuisse.miningman.features.scoreboard;
import ch.bensuisse.miningman.MiningMan;
import ch.bensuisse.miningman.api.ColorUtil;
import io.papermc.paper.scoreboard.numbers.NumberFormat;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.luckperms.api.LuckPerms;
import net.luckperms.api.model.user.User;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Material;
import org.bukkit.Statistic;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.scoreboard.Criteria;
import org.bukkit.scoreboard.DisplaySlot;
import org.bukkit.scoreboard.Objective;
import org.bukkit.scoreboard.Scoreboard;
import org.bukkit.scoreboard.Team;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
/**
* Persönliches Sidebar-Scoreboard: Name, Rang, Tode, Spielzeit, Online-Anzahl, platzierte/abgebaute
* Blöcke, gelaufene Blöcke sowie Server-IP. Standardmäßig für alle Spieler aktiv, kann pro Spieler
* mit /sb ein- oder ausgeblendet werden.
*/
public class ScoreboardService implements Listener {
private static final String OBJECTIVE_NAME = "miningman_sb";
private static final TextColor LABEL_COLOR = TextColor.fromHexString("#FE5A1D");
// Jede Zeile bekommt einen eigenen, satten Farbton - Titel und Icon in derselben Farbe.
private static final TextColor NAME_COLOR = TextColor.fromHexString("#FF5C8A");
private static final TextColor RANK_COLOR = TextColor.fromHexString("#FFD400");
private static final TextColor DEATHS_COLOR = TextColor.fromHexString("#FF3B3B");
private static final TextColor PLAYTIME_COLOR = TextColor.fromHexString("#FFA80F");
private static final TextColor ONLINE_COLOR = TextColor.fromHexString("#39FF6A");
private static final TextColor PLACED_COLOR = TextColor.fromHexString("#B14EFF");
private static final TextColor MINED_COLOR = TextColor.fromHexString("#C68642");
private static final TextColor WALKED_COLOR = TextColor.fromHexString("#22D3EE");
private static final String SEPARATOR = " "; // 25 Zeichen, als Leerzeichen für eine durchgehende Linie
private static final String SERVER_DOMAIN = "ᴍɪɴɪɴɢᴍᴀɴ.eu";
private static final List<Material> MINEABLE_MATERIALS = Arrays.stream(Material.values())
.filter(m -> m.isBlock() && !m.isLegacy())
.toList();
// USE_ITEM verlangt zwingend ein Material, das auch ein Item ist - reine Block-Varianten
// wie ACACIA_WALL_HANGING_SIGN (kein eigenes Item) fliegen sonst mit IllegalArgumentException raus.
private static final List<Material> PLACEABLE_MATERIALS = Arrays.stream(Material.values())
.filter(m -> m.isBlock() && m.isItem() && !m.isLegacy())
.toList();
// Reihenfolge der Zeilen in buildLines(). Nur die dynamischen Indizes werden pro Update
// tatsächlich neu gesendet - Trennlinien, Leerzeilen und die IP-Zeile ändern sich nie.
private static final int LINE_COUNT = 13;
private static final int[] DYNAMIC_LINE_INDICES = { 1, 2, 4, 5, 6, 8, 9, 10 };
// Blöcke platziert/abgebaut erfordern eine Iteration über hunderte Materialien - das lohnt sich
// nicht bei jedem 2-Sekunden-Takt, deshalb wird nur alle paar Zyklen neu gerechnet und zwischengespeichert.
private static final int BLOCK_STATS_REFRESH_INTERVAL = 5; // ~10s bei 2s-Takt
private final MiningMan plugin;
private final LuckPerms luckPerms;
private final List<TextColor> gradientColors;
private final Set<UUID> disabled = new HashSet<>();
private final Map<UUID, PlayerBoard> boards = new HashMap<>();
private final Map<UUID, long[]> blockStatsCache = new HashMap<>();
private int tickCounter = 0;
public ScoreboardService(MiningMan plugin, LuckPerms luckPerms) {
this.plugin = plugin;
this.luckPerms = luckPerms;
this.gradientColors = ColorUtil.createGradient(TextColor.fromHexString("#FE5A1D"),
TextColor.fromHexString("#FFA80F"), 40);
Bukkit.getScheduler().runTaskTimer(plugin, this::updateAll, 20L, 40L);
}
public void onDisable() {
Scoreboard main = Bukkit.getScoreboardManager().getMainScoreboard();
for (Player player : Bukkit.getOnlinePlayers()) {
player.setScoreboard(main);
}
boards.clear();
blockStatsCache.clear();
}
@EventHandler
public void onJoin(PlayerJoinEvent event) {
Bukkit.getScheduler().runTaskLater(plugin, () -> update(event.getPlayer(), true), 10L);
}
@EventHandler
public void onQuit(PlayerQuitEvent event) {
UUID id = event.getPlayer().getUniqueId();
disabled.remove(id);
boards.remove(id);
blockStatsCache.remove(id);
}
/**
* Schaltet das Scoreboard für den Spieler um.
*
* @return true, wenn das Scoreboard danach aktiv ist, sonst false.
*/
public boolean toggle(Player player) {
UUID id = player.getUniqueId();
if (disabled.contains(id)) {
disabled.remove(id);
update(player, true);
return true;
}
disabled.add(id);
boards.remove(id);
player.setScoreboard(Bukkit.getScoreboardManager().getMainScoreboard());
return false;
}
public boolean isEnabled(Player player) {
return !disabled.contains(player.getUniqueId());
}
private void updateAll() {
boolean refreshBlockStats = (tickCounter++ % BLOCK_STATS_REFRESH_INTERVAL) == 0;
for (Player player : Bukkit.getOnlinePlayers()) {
if (isEnabled(player))
update(player, refreshBlockStats);
}
}
private void update(Player player, boolean refreshBlockStats) {
if (!isEnabled(player))
return;
boolean isNewBoard = !boards.containsKey(player.getUniqueId());
PlayerBoard board = boards.computeIfAbsent(player.getUniqueId(), id -> createBoard(player));
List<Component> lines = buildLines(player, refreshBlockStats || isNewBoard);
for (int index : DYNAMIC_LINE_INDICES) {
board.teams[index].prefix(lines.get(index));
}
}
/**
* Registriert Objective, Teams und Scores für einen Spieler genau einmal. Danach werden nur
* noch die Team-Prefixe der dynamischen Zeilen aktualisiert, statt das komplette Scoreboard
* jedes Mal neu aufzubauen und an den Client zu senden.
*/
private PlayerBoard createBoard(Player player) {
Scoreboard scoreboard = Bukkit.getScoreboardManager().getNewScoreboard();
Objective objective = scoreboard.registerNewObjective(OBJECTIVE_NAME, Criteria.DUMMY,
ColorUtil.applyGradient("MiningMan", gradientColors).decorate(TextDecoration.BOLD));
objective.setDisplaySlot(DisplaySlot.SIDEBAR);
objective.numberFormat(NumberFormat.blank());
List<Component> lines = buildLines(player, true);
Team[] teams = new Team[LINE_COUNT];
int score = LINE_COUNT;
for (int i = 0; i < LINE_COUNT; i++) {
String entry = uniqueEntry(score);
Team team = scoreboard.registerNewTeam("l" + i);
team.addEntry(entry);
team.prefix(lines.get(i));
objective.getScore(entry).setScore(score);
teams[i] = team;
score--;
}
player.setScoreboard(scoreboard);
return new PlayerBoard(teams);
}
private List<Component> buildLines(Player player, boolean refreshBlockStats) {
long[] blockStats = getBlockStats(player, refreshBlockStats);
List<Component> lines = new ArrayList<>();
lines.add(separator());
lines.add(nameLine(player));
lines.add(rankLine(player));
lines.add(Component.empty());
lines.add(statLine("☠", DEATHS_COLOR, "ᴅᴇᴀᴛʜs",
String.valueOf(player.getStatistic(Statistic.DEATHS))));
lines.add(statLine("⌛", PLAYTIME_COLOR, "ᴘʟᴀʏᴛɪᴍᴇ",
formatPlaytime(player.getStatistic(Statistic.PLAY_ONE_MINUTE))));
lines.add(statLine("◎", ONLINE_COLOR, "ᴏɴʟɪɴᴇ",
Bukkit.getOnlinePlayers().size() + "/" + Bukkit.getMaxPlayers()));
lines.add(Component.empty());
lines.add(statLine("⧉", PLACED_COLOR, "ᴘʟᴀᴄᴇᴅ", formatNumber(blockStats[1])));
lines.add(statLine("⛏", MINED_COLOR, "ᴍɪɴᴇᴅ", formatNumber(blockStats[0])));
lines.add(statLine("⇄", WALKED_COLOR, "ᴡᴀʟᴋᴇᴅ", formatNumber(getBlocksWalked(player))));
lines.add(separator());
lines.add(ipLine());
return lines;
}
private Component separator() {
return ColorUtil.applyGradient(SEPARATOR, gradientColors).decorate(TextDecoration.STRIKETHROUGH);
}
private Component nameLine(Player player) {
return Component.text(" ✎ ", NAME_COLOR)
.append(Component.text("ɴᴀᴍᴇ: ", NAME_COLOR))
.append(Component.text(player.getName(), NamedTextColor.WHITE).decoration(TextDecoration.BOLD, false));
}
private Component rankLine(Player player) {
User user = luckPerms.getUserManager().getUser(player.getUniqueId());
String prefix = user != null ? user.getCachedData().getMetaData().getPrefix() : null;
Component rankValue;
if (prefix != null && !prefix.isEmpty()) {
rankValue = MiniMessage.miniMessage().deserialize(prefix);
} else {
rankValue = Component.text("-", NamedTextColor.WHITE);
}
return Component.text(" ★ ", RANK_COLOR)
.append(Component.text("ʀᴀɴᴋ: ", RANK_COLOR))
.append(rankValue);
}
private Component statLine(String icon, TextColor color, String label, String value) {
return Component.text(" " + icon + " ", color)
.append(Component.text(label + ": ", color))
.append(Component.text(value, NamedTextColor.WHITE));
}
private Component ipLine() {
return Component.text(" → ", LABEL_COLOR)
.append(Component.text(SERVER_DOMAIN, NamedTextColor.WHITE))
.append(Component.text(" ← ", LABEL_COLOR));
}
/**
* Liefert {abgebaut, platziert} für einen Spieler. Wird nur bei refresh=true neu über alle
* Materialien berechnet, sonst kommt der zwischengespeicherte Wert vom letzten Refresh.
*/
private long[] getBlockStats(Player player, boolean refresh) {
UUID id = player.getUniqueId();
if (refresh || !blockStatsCache.containsKey(id)) {
long[] stats = { getBlocksMined(player), getBlocksPlaced(player) };
blockStatsCache.put(id, stats);
return stats;
}
return blockStatsCache.get(id);
}
/**
* Summiert die Bukkit-Statistik MINE_BLOCK über alle Blockarten (abgebaute Blöcke).
*/
private long getBlocksMined(Player player) {
long total = 0;
for (Material material : MINEABLE_MATERIALS) {
total += player.getStatistic(Statistic.MINE_BLOCK, material);
}
return total;
}
/**
* Summiert die Bukkit-Statistik USE_ITEM über alle platzierbaren Blockarten. Bei platzierbaren
* Blöcken zählt "used" vanilla-seitig genau die Platzierungen (siehe Statistik-Screen im Spiel).
*/
private long getBlocksPlaced(Player player) {
long total = 0;
for (Material material : PLACEABLE_MATERIALS) {
total += player.getStatistic(Statistic.USE_ITEM, material);
}
return total;
}
private long getBlocksWalked(Player player) {
long cm = (long) player.getStatistic(Statistic.WALK_ONE_CM) + player.getStatistic(Statistic.SPRINT_ONE_CM);
return cm / 100L;
}
private String formatPlaytime(int ticks) {
long minutes = ticks / 20L / 60L;
long days = minutes / 1440;
long hours = (minutes % 1440) / 60;
long mins = minutes % 60;
if (days > 0)
return days + "d " + hours + "h";
if (hours > 0)
return hours + "h " + mins + "m";
return mins + "m";
}
/**
* Zeigt Zahlen bis 9999 exakt an (z.B. 9640) und kürzt erst ab 10'000 auf "k" bzw.
* ab 1'000'000 auf "m" (z.B. 12k, 100k, 1m).
*/
private String formatNumber(long value) {
if (value < 10_000)
return String.valueOf(value);
if (value < 1_000_000)
return (value / 1000) + "k";
return (value / 1_000_000) + "m";
}
/**
* Erzeugt eine unsichtbare, eindeutige Score-Entry aus Farbcodes (mehrere Zeilen ohne
* sichtbaren Text sind sonst nicht möglich, da jede Zeile im Scoreboard einen eigenen
* eindeutigen Eintrag benötigt).
*/
private String uniqueEntry(int index) {
StringBuilder sb = new StringBuilder();
int n = index + 1;
while (n > 0) {
sb.append(ChatColor.COLOR_CHAR).append(Integer.toHexString(n % 16));
n /= 16;
}
return sb.toString();
}
private record PlayerBoard(Team[] teams) {
}
}

View File

@ -13,5 +13,6 @@ public class JoinManager implements Listener {
@EventHandler
public void onJoin(PlayerJoinEvent event) {
event.getPlayer().setLevel(2026);
}
}

View File

@ -1,5 +1,5 @@
name: MiningMan
version: '2.0'
version: '1.3'
main: ch.bensuisse.miningman.MiningMan
api-version: '1.21'
authors: [BenSuisse]
@ -18,5 +18,3 @@ commands:
usage: /clearchat
aliases: [cc]
permission: miningman.clearchat
sb:
usage: /sb