Add plugin metrics and update checker (#46)

This commit is contained in:
William 2023-04-21 00:22:07 +01:00 committed by GitHub
parent c48693d865
commit e422bf0840
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 215 additions and 91 deletions

View File

@ -47,7 +47,7 @@ You can include [placeholders](https://william278.net/docs/velocitab/placeholder
PlaceholderAPI placeholders are also supported. To use them, just install [PAPIProxyBridge](https://modrinth.com/plugin/papiproxybridge) on your Velocity proxy and backend Spigot servers. Additionally, a hook for MiniPlaceholders is supported for servers using the MiniMessage formatter.
### Command
You can use the `/velocitab reload` command to reload the plugin config file (permission: `velocitab.command.reload`)
You can use the `/velocitab reload` command to reload the plugin config file (permission: `velocitab.command.reload`), and `/velocitab update` to check for updates (permission: `velocitab.command.update`).
## Building
To build Velocitab, simply run the following in the root of the repository:
@ -64,8 +64,9 @@ Velocitab is licensed under the Apache 2.0 license.
## Links
* **[Website](https://william278.net/project/velocitab)** — Visit my website!
* **[Docs](https://william278.net/docs/velocitab)** — Read the plugin docs!
* **[Modrinth](https://modrinth.com/plugin/velocitab)** — View the plugin Modrinth page
* **[Issues](https://github.com/WiIIiam278/Velocitab/issues)** — File a bug report or feature request
* **[Discord](https://discord.com/invite/tVYhJfyDWG)** — Get support, ask questions!
* **[GitHub](https://github.com/WiIIiam278/Velocitab)** — Check out the plugin source code!
---
© [William278](https://william278.net/), 2023. Licensed under the Apache-2.0 License.

View File

@ -38,7 +38,8 @@ dependencies {
implementation 'net.william278:Annotaml:2.0.1'
implementation 'dev.dejvokep:boosted-yaml:1.3.1'
implementation 'de.themoep:minedown-adventure:1.7.2-SNAPSHOT'
implementation 'net.william278:DesertWell:1.1.1'
implementation 'net.william278:DesertWell:2.0.2'
implementation 'org.bstats:bstats-velocity:3.0.2'
annotationProcessor 'org.projectlombok:lombok:1.18.26'
}
@ -70,6 +71,8 @@ shadowJar {
relocate 'dev.dejvokep.boostedyaml', 'net.william278.velocitab.libraries.boostedyaml'
relocate 'net.william278.annotaml', 'net.william278.velocitab.libraries.annotaml'
relocate 'net.william278.desertwell', 'net.william278.velocitab.libraries.desertwell'
relocate 'org.json', 'net.william278.velocitab.libraries.json'
relocate 'org.bstats', 'net.william278.velocitab.libraries.bstats'
dependencies {
//noinspection GroovyAssignabilityCheck

View File

@ -30,53 +30,64 @@ import com.velocitypowered.api.plugin.annotation.DataDirectory;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.ProxyServer;
import net.william278.annotaml.Annotaml;
import net.william278.desertwell.util.UpdateChecker;
import net.william278.desertwell.util.Version;
import net.william278.velocitab.commands.VelocitabCommand;
import net.william278.velocitab.config.Formatter;
import net.william278.velocitab.config.Settings;
import net.william278.velocitab.hook.Hook;
import net.william278.velocitab.hook.LuckPermsHook;
import net.william278.velocitab.hook.MiniPlaceholdersHook;
import net.william278.velocitab.hook.PapiHook;
import net.william278.velocitab.hook.PAPIProxyBridgeHook;
import net.william278.velocitab.packet.ScoreboardManager;
import net.william278.velocitab.player.Role;
import net.william278.velocitab.player.TabPlayer;
import net.william278.velocitab.tab.PlayerTabList;
import org.bstats.charts.SimplePie;
import org.bstats.velocity.Metrics;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.event.Level;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Path;
import java.util.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Plugin(id = "velocitab")
public class Velocitab {
private static final int METRICS_ID = 18247;
private Settings settings;
private final ProxyServer server;
private final Logger logger;
private final Path dataDirectory;
@Inject
private PluginContainer pluginContainer;
@Inject
private Metrics.Factory metricsFactory;
private PlayerTabList tabList;
private List<Hook> hooks;
private ScoreboardManager scoreboardManager;
@Inject
public Velocitab(ProxyServer server, Logger logger, @DataDirectory Path dataDirectory) {
public Velocitab(@NotNull ProxyServer server, @NotNull Logger logger, @DataDirectory Path dataDirectory) {
this.server = server;
this.logger = logger;
this.dataDirectory = dataDirectory;
}
@Subscribe
public void onProxyInitialization(ProxyInitializeEvent event) {
public void onProxyInitialization(@NotNull ProxyInitializeEvent event) {
loadSettings();
loadHooks();
prepareScoreboardManager();
prepareTabList();
registerCommands();
registerMetrics();
checkForUpdates();
logger.info("Successfully enabled Velocitab");
}
@ -113,12 +124,12 @@ public class Velocitab {
.findFirst();
}
public Optional<LuckPermsHook> getLuckPerms() {
public Optional<LuckPermsHook> getLuckPermsHook() {
return getHook(LuckPermsHook.class);
}
public Optional<PapiHook> getPapiHook() {
return getHook(PapiHook.class);
public Optional<PAPIProxyBridgeHook> getPAPIProxyBridgeHook() {
return getHook(PAPIProxyBridgeHook.class);
}
public Optional<MiniPlaceholdersHook> getMiniPlaceholdersHook() {
@ -155,23 +166,11 @@ public class Velocitab {
@NotNull
public TabPlayer getTabPlayer(@NotNull Player player) {
return new TabPlayer(player,
getLuckPerms().map(hook -> hook.getPlayerRole(player))
.orElse(Role.DEFAULT_ROLE),
getLuckPerms().map(LuckPermsHook::getHighestWeight)
.orElse(0));
}
public void log(@NotNull String message, @NotNull Throwable... exceptions) {
Arrays.stream(exceptions).findFirst().ifPresentOrElse(
exception -> logger.error(message, exception),
() -> logger.warn(message)
getLuckPermsHook().map(hook -> hook.getPlayerRole(player)).orElse(Role.DEFAULT_ROLE),
getLuckPermsHook().map(LuckPermsHook::getHighestWeight).orElse(0)
);
}
public PluginDescription getDescription() {
return pluginContainer.getDescription();
}
private void registerCommands() {
final BrigadierCommand command = new VelocitabCommand(this).command();
server.getCommandManager().register(
@ -179,4 +178,69 @@ public class Velocitab {
command
);
}
@NotNull
public PluginDescription getDescription() {
return pluginContainer.getDescription();
}
@NotNull
public Version getVersion() {
return Version.fromString(getDescription().getVersion().orElseThrow(), "-");
}
private void registerMetrics() {
final Metrics metrics = metricsFactory.make(this, METRICS_ID);
metrics.addCustomChart(new SimplePie("sort_players", () -> settings.isSortPlayers() ? "Enabled" : "Disabled"));
metrics.addCustomChart(new SimplePie("formatter_type", () -> settings.getFormatter().getName()));
metrics.addCustomChart(new SimplePie("using_luckperms", () -> getLuckPermsHook().isPresent() ? "Yes" : "No"));
metrics.addCustomChart(new SimplePie("using_papiproxybridge", () -> getPAPIProxyBridgeHook().isPresent() ? "Yes" : "No"));
metrics.addCustomChart(new SimplePie("using_miniplaceholders", () -> getMiniPlaceholdersHook().isPresent() ? "Yes" : "No"));
}
private void checkForUpdates() {
if (!getSettings().isCheckForUpdates()) {
return;
}
getUpdateChecker().check().thenAccept(checked -> {
if (!checked.isUpToDate()) {
log(Level.WARN, "A new version of Velocitab is available: " + checked.getLatestVersion());
}
});
}
@NotNull
public UpdateChecker getUpdateChecker() {
return UpdateChecker.builder()
.currentVersion(getVersion())
.endpoint(UpdateChecker.Endpoint.MODRINTH)
.resource("velocitab")
.build();
}
public void log(@NotNull Level level, @NotNull String message, @NotNull Throwable... exceptions) {
switch (level) {
case ERROR -> {
if (exceptions.length > 0) {
logger.error(message, exceptions[0]);
} else {
logger.error(message);
}
}
case WARN -> {
if (exceptions.length > 0) {
logger.warn(message, exceptions[0]);
} else {
logger.warn(message);
}
}
case INFO -> logger.info(message);
}
}
public void log(@NotNull String message) {
this.log(Level.INFO, message);
}
}

View File

@ -25,62 +25,81 @@ import com.velocitypowered.api.command.BrigadierCommand;
import com.velocitypowered.api.command.CommandSource;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.TextColor;
import net.william278.desertwell.AboutMenu;
import net.william278.desertwell.Version;
import net.william278.desertwell.about.AboutMenu;
import net.william278.velocitab.Velocitab;
import org.jetbrains.annotations.NotNull;
public final class VelocitabCommand {
private static final TextColor MAIN_COLOR = TextColor.color(0x00FB9A);
private final AboutMenu aboutMenu;
private final Velocitab plugin;
public VelocitabCommand(final @NotNull Velocitab plugin) {
this.plugin = plugin;
this.aboutMenu = AboutMenu.create("Velocitab")
.withDescription(plugin.getDescription().getDescription().orElseThrow())
.withVersion(Version.fromString(plugin.getDescription().getVersion().orElseThrow(), "-"))
.addAttribution("Author",
AboutMenu.Credit.of("William278").withDescription("Click to visit website").withUrl("https://william278.net"))
.addAttribution("Contributors",
AboutMenu.Credit.of("Ironboundred").withDescription("Coding"),
AboutMenu.Credit.of("Emibergo02").withDescription("Coding"),
AboutMenu.Credit.of("FreeMonoid").withDescription("Coding"),
AboutMenu.Credit.of("4drian3d").withDescription("Coding"))
.addButtons(
AboutMenu.Link.of("https://william278.net/docs/velocitab").withText("Docs").withIcon(""),
AboutMenu.Link.of("https://discord.gg/tVYhJfyDWG").withText("Discord").withIcon("").withColor("#6773f5"),
AboutMenu.Link.of("https://modrinth.com/plugin/velocitab").withText("Modrinth").withIcon("").withColor("#589143"));
this.aboutMenu = AboutMenu.builder()
.title(Component.text("Velocitab"))
.description(Component.text(plugin.getDescription().getDescription().orElseThrow()))
.version(plugin.getVersion())
.credits("Author",
AboutMenu.Credit.of("William278").description("Click to visit website").url("https://william278.net"))
.credits("Contributors",
AboutMenu.Credit.of("Ironboundred").description("Coding"),
AboutMenu.Credit.of("Emibergo02").description("Coding"),
AboutMenu.Credit.of("FreeMonoid").description("Coding"),
AboutMenu.Credit.of("4drian3d").description("Coding"))
.buttons(
AboutMenu.Link.of("https://william278.net/docs/velocitab").text("Docs").icon(""),
AboutMenu.Link.of("https://discord.gg/tVYhJfyDWG").text("Discord").icon("").color(TextColor.color(0x6773f5)),
AboutMenu.Link.of("https://modrinth.com/plugin/velocitab").text("Modrinth").icon("").color(TextColor.color(0x589143)))
.build();
}
@NotNull
public BrigadierCommand command() {
final LiteralArgumentBuilder<CommandSource> builder = LiteralArgumentBuilder
.<CommandSource>literal("velocitab")
.executes(ctx -> {
sendAboutInfo(ctx.getSource());
return Command.SINGLE_SUCCESS;
})
.then(LiteralArgumentBuilder.<CommandSource>literal("about")
.executes(ctx -> {
sendAboutInfo(ctx.getSource());
return Command.SINGLE_SUCCESS;
})
)
.then(LiteralArgumentBuilder.<CommandSource>literal("reload")
.requires(src -> src.hasPermission("velocitab.command.reload"))
.executes(ctx -> {
plugin.loadSettings();
plugin.getTabList().reloadUpdate();
ctx.getSource().sendMessage(Component.text(
"Velocitab has been reloaded!",
TextColor.color(255, 199, 31)));
return Command.SINGLE_SUCCESS;
})
);
.<CommandSource>literal("velocitab")
.executes(ctx -> {
sendAboutInfo(ctx.getSource());
return Command.SINGLE_SUCCESS;
})
.then(LiteralArgumentBuilder.<CommandSource>literal("about")
.executes(ctx -> {
sendAboutInfo(ctx.getSource());
return Command.SINGLE_SUCCESS;
})
)
.then(LiteralArgumentBuilder.<CommandSource>literal("reload")
.requires(src -> src.hasPermission("velocitab.command.reload"))
.executes(ctx -> {
plugin.loadSettings();
plugin.getTabList().reloadUpdate();
ctx.getSource().sendMessage(Component.text(
"Velocitab has been reloaded!",
MAIN_COLOR));
return Command.SINGLE_SUCCESS;
})
)
.then(LiteralArgumentBuilder.<CommandSource>literal("update")
.requires(src -> src.hasPermission("velocitab.command.update"))
.executes(ctx -> {
plugin.getUpdateChecker().check().thenAccept(checked -> {
if (checked.isUpToDate()) {
ctx.getSource().sendMessage(Component
.text("Velocitab is up to date! (Running v" + plugin.getVersion() + ")", MAIN_COLOR));
return;
}
ctx.getSource().sendMessage(Component
.text("An update for velocitab is available. " +
"Please update to " + checked.getLatestVersion(), MAIN_COLOR));
});
return Command.SINGLE_SUCCESS;
})
);
return new BrigadierCommand(builder);
}
private void sendAboutInfo(CommandSource source) {
source.sendMessage(aboutMenu.toMineDown().toComponent());
private void sendAboutInfo(@NotNull CommandSource source) {
source.sendMessage(aboutMenu.toComponent());
}
}

View File

@ -35,14 +35,28 @@ import java.util.function.Function;
*/
@SuppressWarnings("unused")
public enum Formatter {
MINEDOWN((text, player, plugin) -> new MineDown(text).toComponent(),
(text) -> text.replace("__", "_\\_")),
MINIMESSAGE((text, player, plugin) -> plugin.getMiniPlaceholdersHook()
.map(hook -> hook.format(text, player.getPlayer()))
.orElse(MiniMessage.miniMessage().deserialize(text)),
(text) -> MiniMessage.miniMessage().escapeTags(text)),
LEGACY((text, player, plugin) -> LegacyComponentSerializer.legacyAmpersand().deserialize(text),
Function.identity());
MINEDOWN(
(text, player, plugin) -> new MineDown(text).toComponent(),
(text) -> text.replace("__", "_\\_"),
"MineDown"
),
MINIMESSAGE(
(text, player, plugin) -> plugin.getMiniPlaceholdersHook()
.map(hook -> hook.format(text, player.getPlayer()))
.orElse(MiniMessage.miniMessage().deserialize(text)),
(text) -> MiniMessage.miniMessage().escapeTags(text),
"MiniMessage"
),
LEGACY(
(text, player, plugin) -> LegacyComponentSerializer.legacyAmpersand().deserialize(text),
Function.identity(),
"Legacy Text"
);
/**
* Name of the formatter
*/
private final String name;
/**
* Function to apply formatting to a string
@ -53,9 +67,11 @@ public enum Formatter {
*/
private final Function<String, String> escaper;
Formatter(@NotNull TriFunction<String, TabPlayer, Velocitab, Component> formatter, @NotNull Function<String, String> escaper) {
Formatter(@NotNull TriFunction<String, TabPlayer, Velocitab, Component> formatter, @NotNull Function<String, String> escaper,
@NotNull String name) {
this.formatter = formatter;
this.escaper = escaper;
this.name = name;
}
@NotNull
@ -68,4 +84,9 @@ public enum Formatter {
return escaper.apply(text);
}
@NotNull
public String getName() {
return name;
}
}

View File

@ -64,8 +64,8 @@ public enum Placeholder {
}
final String replaced = format;
return plugin.getPapiHook()
.map(hook -> hook.formatPapiPlaceholders(replaced, player.getPlayer()))
return plugin.getPAPIProxyBridgeHook()
.map(hook -> hook.formatPlaceholders(replaced, player.getPlayer()))
.orElse(CompletableFuture.completedFuture(replaced));
}

View File

@ -36,9 +36,15 @@ import java.util.Map;
Velocitab Config
Developed by William278
Placeholders: %players_online%, %max_players_online%, %local_players_online%, %current_date%, %current_time%, %username%, %server%, %ping%, %prefix%, %suffix%, %role%""")
Information: https://william278.net/project/velocitab
Documentation: https://william278.net/docs/velocitab""")
public class Settings {
@Getter
@YamlKey("check_for_updates")
@YamlComment("Check for updates on startup")
private boolean checkForUpdates = true;
@YamlKey("headers")
@YamlComment("Header(s) to display above the TAB list for each server group.\nList multiple headers and set update_rate to the number of ticks between frames for basic animations")
private Map<String, List<String>> headers = Map.of("default", List.of("&rainbow&Running Velocitab by William278"));

View File

@ -21,6 +21,7 @@ package net.william278.velocitab.hook;
import net.william278.velocitab.Velocitab;
import org.jetbrains.annotations.NotNull;
import org.slf4j.event.Level;
import java.util.List;
import java.util.Optional;
@ -35,7 +36,7 @@ public abstract class Hook {
plugin.log("Successfully hooked into LuckPerms");
return Optional.of(new LuckPermsHook(plugin));
} catch (Exception e) {
plugin.log("LuckPerms hook was not loaded: " + e.getMessage(), e);
plugin.log(Level.WARN, "LuckPerms hook was not loaded: " + e.getMessage(), e);
}
}
return Optional.empty();
@ -44,9 +45,9 @@ public abstract class Hook {
if (isPluginAvailable(plugin, "papiproxybridge") && plugin.getSettings().isEnablePapiHook()) {
try {
plugin.log("Successfully hooked into PAPIProxyBridge");
return Optional.of(new PapiHook(plugin));
return Optional.of(new PAPIProxyBridgeHook(plugin));
} catch (Exception e) {
plugin.log("PAPIProxyBridge hook was not loaded: " + e.getMessage(), e);
plugin.log(Level.WARN, "PAPIProxyBridge hook was not loaded: " + e.getMessage(), e);
}
}
return Optional.empty();
@ -57,7 +58,7 @@ public abstract class Hook {
plugin.log("Successfully hooked into MiniPlaceholders");
return Optional.of(new MiniPlaceholdersHook(plugin));
} catch (Exception e) {
plugin.log("MiniPlaceholders hook was not loaded: " + e.getMessage(), e);
plugin.log(Level.WARN, "MiniPlaceholders hook was not loaded: " + e.getMessage(), e);
}
}
return Optional.empty();

View File

@ -26,16 +26,16 @@ import org.jetbrains.annotations.NotNull;
import java.util.concurrent.CompletableFuture;
public class PapiHook extends Hook {
public class PAPIProxyBridgeHook extends Hook {
private final PlaceholderAPI api;
public PapiHook(@NotNull Velocitab plugin) {
public PAPIProxyBridgeHook(@NotNull Velocitab plugin) {
super(plugin);
this.api = PlaceholderAPI.getInstance();
}
public CompletableFuture<String> formatPapiPlaceholders(@NotNull String input, @NotNull Player player) {
public CompletableFuture<String> formatPlaceholders(@NotNull String input, @NotNull Player player) {
return api.formatPlaceholders(input, player.getUniqueId());
}

View File

@ -25,11 +25,13 @@ import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.StateRegistry;
import net.william278.velocitab.Velocitab;
import org.jetbrains.annotations.NotNull;
import org.slf4j.event.Level;
import java.util.*;
import java.util.stream.Collectors;
import static com.velocitypowered.api.network.ProtocolVersion.*;
public class ScoreboardManager {
private final Velocitab plugin;
@ -90,7 +92,7 @@ public class ScoreboardManager {
final ConnectedPlayer connectedPlayer = (ConnectedPlayer) player;
connectedPlayer.getConnection().write(packet);
} catch (Exception e) {
plugin.log("Failed to dispatch packet (is the client or server modded or using an illegal version?)", e);
plugin.log(Level.ERROR, "Failed to dispatch packet (is the client or server modded or using an illegal version?)", e);
}
}
@ -109,7 +111,7 @@ public class ScoreboardManager {
.mapping(0x5A, MINECRAFT_1_19_4, false)
.register();
} catch (Throwable e) {
plugin.log("Failed to register UpdateTeamsPacket", e);
plugin.log(Level.ERROR, "Failed to register UpdateTeamsPacket", e);
}
}

View File

@ -226,7 +226,9 @@ public class PlayerTabList {
.schedule();
}
// Update all players since there was a reload of the config
/**
* Update the TAB list for all players when a plugin or proxy reload is performed
*/
public void reloadUpdate() {
if (players.isEmpty()) {
return;
@ -248,9 +250,10 @@ public class PlayerTabList {
}
/**
* Get the servers in the same group as the given server
* If the server is not in a group, use fallback
* If fallback is disabled, return empty
* Get the servers in the same group as the given server, as an optional
* <p>
* If the server is not in a group, use the fallback group
* If the fallback is disabled, return an empty optional
*
* @param serverName The server name
* @return The servers in the same group as the given server, empty if the server is not in a group and fallback is disabled
@ -279,8 +282,12 @@ public class PlayerTabList {
plugin.log("Velocitab has been reloaded!");
}
/**
* Remove an offline player from the list of tracked TAB players
*
* @param player The player to remove
*/
public void removeOfflinePlayer(@NotNull Player player) {
// Try and remove the player from the list of players
if (!players.removeIf(tabPlayer -> tabPlayer.getPlayer().getUniqueId().equals(player.getUniqueId()))) {
plugin.log("Failed to remove offline player " + player.getUsername() + " (UUID: " + player.getUniqueId() + ")");
}