Compare commits

..
24 Commits
Author SHA1 Message Date
jmp 9d6d7feae0 Add config option for whether the displayed online player count is allowed to exceed the displayed maximum player count 2021-04-08 01:10:53 -07:00
jmp ed6595ee0d Refactor player count modification 2021-04-07 03:54:25 -07:00
jmp a462f0b96e Minor formatting changes 2021-04-07 01:26:14 -07:00
Mark Joseph KuebelandMark Kuebel af4f0c78f1 Added a minimum player fake player count, and a constant count. (#50)
* Added a minimum player fake player count, and a constant count.

* updates grammars and stuffs

* removes periods in config example

Co-authored-by: Mark Kuebel <mjkuebel@cs.utexas.edu>
2021-04-06 16:43:06 -07:00
jmp 7ef0ada06a Build script changes
Switch from Spigradle to Minecrell's plugin-yml plugin
2021-04-04 15:55:59 -07:00
jmp fedae0ab23 Clean up IconManager 2021-04-04 15:48:35 -07:00
jmp 157bc7b767 velocity: Remove manual component color downsampling as Velocity handles it automatically 2021-03-28 22:29:53 -07:00
jmp 71f653122a bungee: Stupid hacks to workaround Travertine issues with <=1.8 clients 2021-03-27 23:33:14 -07:00
Renovate Bot b44070bdb0 chore(deps): update dependency net.kyori.blossom:net.kyori.blossom.gradle.plugin to v1.2.0 2021-03-23 15:08:26 -07:00
jmp ae5ab6a95b sponge8: Fix logging 2021-03-22 23:21:28 -07:00
Jason 73ee4f393b Update README.md 2021-03-22 05:52:31 -07:00
jmp ff4a1da49c Implement Sponge API 7 platform 2021-03-22 04:40:33 -07:00
jmp 725e5c8d2b sponge: Update for API changes 2021-03-21 14:47:33 -07:00
jmp 5d644c326a Bump version to 2.0.2-SNAPSHOT 2021-03-19 23:15:23 -07:00
jmp 09683f06ce Implement 'hide-player-count' option
Setting this to true will disable the player list hover (same as 'disable-player-list-hover'), but will also cause the player count to appear as '???'.
2021-03-16 15:31:22 -07:00
jmp c92c718ced Add ComponentColorDownsampler 2021-03-13 18:06:24 -08:00
jmp e468600d83 Parse MOTD MiniMessage in a single place
as opposed to in each platform
2021-03-12 15:35:40 -08:00
Renovate Bot 101915290e chore(deps): update dependency org.spongepowered.gradle.plugin:org.spongepowered.gradle.plugin.gradle.plugin to v1.0.2 2021-03-11 15:40:42 -08:00
Jason b0c8c7a11d Update issue templates 2021-03-11 14:57:42 -08:00
Jason 5bac39d149 Create config.yml 2021-03-11 14:56:15 -08:00
Jason 37dd141af4 Update bug-report.md 2021-03-11 14:47:59 -08:00
jmp d348ee6b1d Update loom and Adventure 2021-03-03 01:28:25 -08:00
jmp b1336df817 Work around issue with shadowJar and the sponge runServer task 2021-02-28 13:01:32 -08:00
jmp 8e9c8aac5f Refactor commands, implement commands on sponge-8 2021-02-28 03:38:27 -08:00
50 changed files with 1481 additions and 728 deletions

No files matched your search

+2 -2
View File
@@ -2,7 +2,7 @@
name: Bug Report
about: Report a bug
title: "[Bug Report] This happens when another thing should have"
labels: bug
labels: bug, unconfirmed
assignees: ''
---
@@ -24,7 +24,7 @@ A clear and concise description of what you expected to happen.
If applicable, add screenshots to help explain your problem.
**Server Software and Version:**
- Vendor: [e.g. Paper, Spigot, Tuinity, Velocity, BungeeCord, Waterfall]
- Vendor: [e.g. Paper, Tuinity, Velocity, BungeeCord, Waterfall, Fabric, Sponge]
- Minecraft Version: [e.g. 1.15.2, 1.16.3]
- Output of /version command: [e.g. This server is running Paper version git-Paper-53 (MC: 1.16.1) (Implementing API version 1.16.1-R0.1-SNAPSHOT)]
+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: false
+1
View File
@@ -12,6 +12,7 @@
- [Paper](https://papermc.io/) / Spigot
- MiniMOTD is compatible with Spigot, however many features will not work. It is recommended to use Paper for full compatibility.
- [Sponge API 8](https://www.spongepowered.org/)
- [Sponge API 7](https://www.spongepowered.org/)
- [Fabric](https://fabricmc.net/) (requires [Fabric API](https://www.curseforge.com/minecraft/mc-mods/fabric-api))
#### Proxy Platforms
+36 -50
View File
@@ -1,3 +1,5 @@
import com.adarshr.gradle.testlogger.TestLoggerPlugin
import com.adarshr.gradle.testlogger.theme.ThemeType
import com.github.jengelman.gradle.plugins.shadow.ShadowPlugin
import net.kyori.indra.IndraCheckstylePlugin
import net.kyori.indra.IndraLicenseHeaderPlugin
@@ -9,15 +11,17 @@ plugins {
`java-library`
id("net.kyori.indra") version "1.3.1"
id("com.github.johnrengelman.shadow") version "6.1.0"
id("com.adarshr.test-logger") version "3.0.0"
id("net.kyori.blossom") version "1.2.0" apply false
}
allprojects {
group = "xyz.jpenilla"
version = "2.0.1+${latestCommitHash()}-SNAPSHOT"
description = "Use MiniMessage text formatting in your servers MOTD."
version = "2.0.2+${lastCommitHash()}-SNAPSHOT"
description = "Use MiniMessage text formatting in the server list MOTD."
}
ext["url"] = "https://github.com/jpenilla/MiniMOTD/"
ext["url"] = "https://github.com/jpenilla/MiniMOTD"
subprojects {
apply<JavaLibraryPlugin>()
@@ -25,6 +29,7 @@ subprojects {
apply<IndraPlugin>()
apply<IndraCheckstylePlugin>()
apply<IndraLicenseHeaderPlugin>()
apply<TestLoggerPlugin>()
repositories {
//mavenLocal()
@@ -41,76 +46,57 @@ subprojects {
maven("https://repo.codemc.org/repository/maven-public")
}
dependencies {
testImplementation("org.junit.jupiter", "junit-jupiter-engine", "5.7.0")
}
indra {
javaVersions.target.set(8)
javaVersions {
testWith(8, 11, 15)
target.set(8)
}
github("jpenilla", "MiniMOTD") {
issues = true
}
mitLicense()
}
testlogger {
theme = ThemeType.MOCHA_PARALLEL
showPassed = false
}
tasks {
shadowJar {
minimize()
exclude("META-INF/versions/**")
if (!project.name.contains("fabric")) {
archiveClassifier.set("")
doLast {
val output = outputs.files.singleFile
output.copyTo(rootProject.buildDir.resolve("libs").resolve(output.name), overwrite = true)
}
}
}
withType<JavaCompile> {
options.compilerArgs.add("-Xlint:-processing")
}
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
}
allprojects {
tasks.withType<Jar> {
onlyIf {
val classifier = archiveClassifier.get()
classifier != "javadoc"
&& project.name != rootProject.name
withType<Jar> {
onlyIf { archiveClassifier.get() != "javadoc" }
}
withType<Javadoc> {
onlyIf { false }
}
}
tasks.withType<Javadoc> {
onlyIf { false }
}
}
configurations.archives {
artifacts.removeAll { true }
tasks.withType<Jar> {
onlyIf { false }
}
tasks {
val aggregate = create("aggregateJars") {
val artifacts = arrayListOf<File>()
dependsOn(project(":minimotd-universal").tasks.getByName("build"))
arrayOf("spigot", "bungeecord", "velocity", "sponge8").forEach {
val subProject = project(":minimotd-$it")
val shadow = subProject.tasks.getByName("shadowJar")
artifacts.add(shadow.outputs.files.singleFile)
}
doLast {
artifacts.add(project(":minimotd-fabric").tasks.getByName("remapJar").outputs.files.singleFile)
artifacts.add(project(":minimotd-universal").project.tasks.getByName("universal").outputs.files.singleFile)
val libs = rootProject.buildDir.resolve("libs")
libs.listFiles()?.forEach { it.delete() }
artifacts.forEach { it.copyTo(libs.resolve(it.name)) }
}
}
build {
dependsOn(aggregate)
}
}
fun latestCommitHash(): String {
val byteOut = ByteArrayOutputStream()
fun lastCommitHash(): String = ByteArrayOutputStream().apply {
exec {
commandLine = listOf("git", "rev-parse", "--short", "HEAD")
standardOutput = byteOut
standardOutput = this@apply
}
return byteOut.toString(Charsets.UTF_8.name()).trim()
}
}.toString(Charsets.UTF_8.name()).trim()
+3 -2
View File
@@ -1,12 +1,12 @@
plugins {
id("kr.entree.spigradle.bungee") version "2.2.3"
id("net.minecrell.plugin-yml.bungee") version "0.3.0"
}
dependencies {
implementation(project(":minimotd-common"))
implementation("org.slf4j", "slf4j-jdk14","1.7.30")
implementation("net.kyori", "adventure-platform-bungeecord", "4.0.0-SNAPSHOT")
implementation("org.bstats", "bstats-bungeecord", "2.2.0")
implementation("org.bstats", "bstats-bungeecord", "2.2.1")
compileOnly("io.github.waterfallmc", "waterfall-api", "1.16-R0.4-SNAPSHOT")
}
@@ -27,5 +27,6 @@ tasks {
bungee {
name = rootProject.name
main = "xyz.jpenilla.minimotd.bungee.MiniMOTDPlugin"
author = "jmp"
}
@@ -23,93 +23,58 @@
*/
package xyz.jpenilla.minimotd.bungee;
import net.kyori.adventure.identity.Identity;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.plugin.Command;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.ArrayList;
import java.util.List;
import xyz.jpenilla.minimotd.common.CommandHandlerFactory;
public class BungeeCommand extends Command {
private final MiniMOTDPlugin plugin;
private final MiniMessage miniMessage;
private final CommandHandlerFactory handlerFactory;
public BungeeCommand(final @NonNull MiniMOTDPlugin plugin) {
super("minimotd");
this.plugin = plugin;
this.miniMessage = MiniMessage.get();
this.handlerFactory = new CommandHandlerFactory(plugin.miniMOTD());
}
@Override
public void execute(final @NonNull CommandSender sender, final @NonNull String @NonNull [] args) {
final Audience audience = this.plugin.audiences().sender(sender);
if (!sender.hasPermission("minimotd.admin")) {
this.onNoPermission(sender, args);
audience.sendMessage(Component.text("No permission.", NamedTextColor.RED));
return;
}
if (args.length == 0) {
this.onInvalidUse(sender, args);
this.onInvalidUse(audience);
return;
}
switch (args[0]) {
case "about":
this.onAbout(sender, args);
this.handlerFactory.about().execute(audience);
return;
case "help":
this.onHelp(sender, args);
this.handlerFactory.help().execute(audience);
return;
case "reload":
this.onReload(sender, args);
this.handlerFactory.reload().execute(audience);
return;
}
this.onInvalidUse(sender, args);
this.onInvalidUse(audience);
}
private void onHelp(final @NonNull CommandSender sender, final @NonNull String[] args) {
final List<String> messages = new ArrayList<>();
messages.add("<gradient:blue:green>MiniMOTD Commands:");
messages.add(" <gray>-</gray> <hover:show_text:'<green>Click for <yellow>/minimotd about'><click:run_command:/minimotd about><yellow>/minimotd about");
messages.add(" <gray>-</gray> <hover:show_text:'<green>Click for <yellow>/minimotd reload'><click:run_command:/minimotd reload><yellow>/minimotd reload");
messages.add(" <gray>-</gray> <hover:show_text:'<green>Click for <yellow>/minimotd help'><click:run_command:/minimotd help><yellow>/minimotd help");
this.send(sender, messages);
}
private void onReload(final @NonNull CommandSender sender, final @NonNull String[] args) {
this.plugin.miniMOTD().configManager().loadConfigs();
this.plugin.miniMOTD().configManager().loadExtraConfigs();
this.plugin.miniMOTD().iconManager().loadIcons();
this.send(sender, "<green>Done reloading.");
}
private void onAbout(final @NonNull CommandSender sender, final @NonNull String[] args) {
final ArrayList<String> messages = new ArrayList<>();
final String header = "<gradient:white:black>=============</gradient><gradient:black:white>=============";
messages.add(header);
messages.add("<hover:show_text:'<rainbow>click me!'><click:open_url:https://github.com/jmanpenilla/MiniMOTD/>" + this.plugin.getDescription().getName() + " <gradient:red:yellow>" + this.plugin.getDescription().getVersion());
messages.add("<yellow>By</yellow><gray>:</gray> <gradient:blue:green>jmp");
messages.add(header);
this.send(sender, messages);
}
private void onInvalidUse(final @NonNull CommandSender sender, final @NonNull String[] args) {
this.send(sender, "<hover:show_text:'<green>Click for <yellow>/minimotd help'><click:run_command:/minimotd help><italic><gradient:red:gold>Invalid usage.</gradient> <blue>Try <yellow>/minimotd help</yellow> or click here");
}
private void onNoPermission(final @NonNull CommandSender sender, final @NonNull String[] args) {
this.send(sender, "<gradient:red:yellow>No permission.");
}
private void send(final @NonNull CommandSender sender, final @NonNull String message) {
this.plugin.audiences().sender(sender).sendMessage(Identity.nil(), this.miniMessage.parse(message));
}
private void send(final @NonNull CommandSender sender, final @NonNull List<String> messages) {
for (final String message : messages) {
this.send(sender, message);
}
private void onInvalidUse(final @NonNull Audience audience) {
audience.sendMessage(
Component.text("Invalid command usage. Use '/minimotd help' for a list of command provided by MiniMOTD.", NamedTextColor.RED)
.hoverEvent(Component.text("Click to execute '/minimotd help'"))
.clickEvent(ClickEvent.runCommand("/minimotd help"))
);
}
}
@@ -23,11 +23,15 @@
*/
package xyz.jpenilla.minimotd.bungee;
import com.google.gson.Gson;
import net.kyori.adventure.platform.bungeecord.BungeeAudiences;
import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer;
import net.md_5.bungee.api.Favicon;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.plugin.Plugin;
import org.bstats.bungeecord.Metrics;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import xyz.jpenilla.minimotd.common.MiniMOTD;
@@ -35,6 +39,7 @@ import xyz.jpenilla.minimotd.common.MiniMOTDPlatform;
import xyz.jpenilla.minimotd.common.UpdateChecker;
import java.awt.image.BufferedImage;
import java.lang.reflect.Field;
import java.nio.file.Path;
public class MiniMOTDPlugin extends Plugin implements MiniMOTDPlatform<Favicon> {
@@ -52,13 +57,14 @@ public class MiniMOTDPlugin extends Plugin implements MiniMOTDPlatform<Favicon>
this.miniMOTD = new MiniMOTD<>(this);
this.miniMOTD.configManager().loadExtraConfigs();
this.audiences = BungeeAudiences.create(this);
this.injectTravertineGson();
this.getProxy().getPluginManager().registerListener(this, new PingListener(this.miniMOTD));
this.getProxy().getPluginManager().registerCommand(this, new BungeeCommand(this));
final Metrics metrics = new Metrics(this, 8137);
if (this.miniMOTD.configManager().pluginSettings().updateChecker()) {
this.getProxy().getScheduler().runAsync(this, () ->
new UpdateChecker(this.getDescription().getVersion()).checkVersion().forEach(getLogger()::info));
new UpdateChecker().checkVersion().forEach(this.logger::info));
}
}
@@ -80,4 +86,28 @@ public class MiniMOTDPlugin extends Plugin implements MiniMOTDPlatform<Favicon>
public @NonNull BungeeAudiences audiences() {
return this.audiences;
}
@Override
public void onReload() {
this.miniMOTD.configManager().loadExtraConfigs();
}
private void injectTravertineGson() {
final Field gsonLegacyField = findDeclaredField(ProxyServer.getInstance().getClass(), "gsonLegacy");
if (gsonLegacyField != null) {
try {
BungeeComponentSerializer.inject((Gson) gsonLegacyField.get(ProxyServer.getInstance()));
} catch (final IllegalAccessException ex) {
this.miniMOTD.logger().warn("Failed to inject into Travertine's gsonLegacy gson instance. There will likely be issues with 1.7.x clients.", ex);
}
}
}
private static @Nullable Field findDeclaredField(final @NonNull Class<?> holder, final @NonNull String name) {
try {
return holder.getDeclaredField(name);
} catch (final NoSuchFieldException ex) {
return null;
}
}
}
@@ -24,72 +24,80 @@
package xyz.jpenilla.minimotd.bungee;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.md_5.bungee.api.Favicon;
import net.md_5.bungee.api.ServerPing;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.event.ProxyPingEvent;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.event.EventHandler;
import net.md_5.bungee.event.EventPriority;
import org.checkerframework.checker.nullness.qual.NonNull;
import xyz.jpenilla.minimotd.common.Constants;
import xyz.jpenilla.minimotd.common.MOTDIconPair;
import xyz.jpenilla.minimotd.common.MiniMOTD;
import xyz.jpenilla.minimotd.common.config.MiniMOTDConfig;
import xyz.jpenilla.minimotd.common.config.MiniMOTDConfig.PlayerCount;
import java.util.Optional;
public class PingListener implements Listener {
private final MiniMessage miniMessage = MiniMessage.get();
private final MiniMOTD<Favicon> miniMOTD;
public PingListener(final @NonNull MiniMOTD<Favicon> miniMOTD) {
this.miniMOTD = miniMOTD;
}
@EventHandler(priority = EventPriority.LOWEST)
@EventHandler
public void onPing(final @NonNull ProxyPingEvent e) {
final ServerPing response = e.getResponse();
if (response == null) {
return;
}
if (response != null) {
final String configString = this.miniMOTD.configManager().pluginSettings().configStringForHost(
Optional.ofNullable(e.getConnection().getVirtualHost())
.map(inetSocketAddress -> String.format("%s:%s", inetSocketAddress.getHostName(), inetSocketAddress.getPort()))
.orElse("default")
).orElse("default");
final MiniMOTDConfig cfg = this.miniMOTD.configManager().resolveConfig(configString);
final ServerPing.Players players = response.getPlayers();
final int onlinePlayers = this.miniMOTD.calculateOnlinePlayers(cfg, players.getOnline());
final String configString = this.miniMOTD.configManager().pluginSettings().configStringForHost(
Optional.ofNullable(e.getConnection().getVirtualHost())
.map(inetSocketAddress -> String.format("%s:%s", inetSocketAddress.getHostName(), inetSocketAddress.getPort()))
.orElse("default")
).orElse("default");
final MiniMOTDConfig cfg = this.miniMOTD.configManager().resolveConfig(configString);
final ServerPing.Players players = response.getPlayers();
final PlayerCount count = cfg.modifyPlayerCount(players.getOnline(), players.getMax());
final int onlinePlayers = count.onlinePlayers();
final int maxPlayers = count.maxPlayers();
if (cfg.hidePlayerCount()) {
response.setPlayers(null);
} else {
players.setOnline(onlinePlayers);
final int maxPlayers = cfg.adjustedMaxPlayers(onlinePlayers, players.getMax());
players.setMax(maxPlayers);
if (cfg.disablePlayerListHover()) {
players.setSample(new ServerPing.PlayerInfo[]{});
}
final MOTDIconPair<Favicon> pair = this.miniMOTD.createMOTD(cfg, onlinePlayers, maxPlayers);
final String motdString = pair.motd();
if (motdString != null) {
Component motdComponent = this.miniMessage.parse(motdString);
if (e.getConnection().getVersion() < Constants.MINECRAFT_1_16_PROTOCOL_VERSION) {
motdComponent = GsonComponentSerializer.colorDownsamplingGson().deserialize(GsonComponentSerializer.colorDownsamplingGson().serialize(motdComponent));
}
response.setDescriptionComponent(new TextComponent(BungeeComponentSerializer.get().serialize(motdComponent)));
}
final Favicon favicon = pair.icon();
if (favicon != null) {
response.setFavicon(favicon);
}
response.setPlayers(players);
e.setResponse(response);
}
final MOTDIconPair<Favicon> pair = this.miniMOTD.createMOTD(cfg, onlinePlayers, maxPlayers);
final Component motdComponent = pair.motd();
if (motdComponent != null) {
final BaseComponent[] bungee;
if (e.getConnection().getVersion() < Constants.MINECRAFT_1_16_PROTOCOL_VERSION) {
bungee = BungeeComponentSerializer.legacy().serialize(motdComponent);
} else {
bungee = BungeeComponentSerializer.get().serialize(motdComponent);
}
if (BungeeComponentSerializer.isNative()) {
response.setDescriptionComponent(bungee[0]);
} else {
response.setDescriptionComponent(new TextComponent(bungee));
}
}
final Favicon favicon = pair.icon();
if (favicon != null) {
response.setFavicon(favicon);
}
e.setResponse(response);
}
}
+27 -1
View File
@@ -1,8 +1,34 @@
plugins {
id("net.kyori.blossom")
}
tasks.jar {
from(rootProject.file("license.txt")) {
rename { "license_${rootProject.name.toLowerCase()}.txt" }
}
}
dependencies {
api("org.slf4j", "slf4j-api", "1.7.30")
api("org.spongepowered", "configurate-hocon", "4.1.0-SNAPSHOT")
api("net.kyori", "adventure-api", "4.3.0")
platform(api("net.kyori", "adventure-bom", "4.7.0"))
api("net.kyori", "adventure-text-serializer-gson") {
exclude("com.google.code.gson", "gson")
}
api("net.kyori", "adventure-text-minimessage", "4.1.0-SNAPSHOT")
compileOnlyApi("com.google.code.gson", "gson", "2.8.0")
testImplementation("com.google.code.gson", "gson", "2.8.0")
compileOnlyApi("com.google.guava", "guava", "21.0")
}
blossom {
fun replaceTokens(file: String, vararg tokens: Pair<String, String>) = tokens.forEach { (k, v) ->
replaceToken("\${$k}", v, file)
}
replaceTokens(
"src/main/java/xyz/jpenilla/minimotd/common/Constants.java",
"PLUGIN_NAME" to rootProject.name,
"PLUGIN_VERSION" to project.version.toString(),
"PLUGIN_WEBSITE" to rootProject.ext["url"].toString()
)
}
@@ -0,0 +1,124 @@
/*
* This file is part of MiniMOTD, licensed under the MIT License.
*
* Copyright (c) 2021 Jason Penilla
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package xyz.jpenilla.minimotd.common;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.stream.Stream;
import static net.kyori.adventure.text.Component.space;
import static net.kyori.adventure.text.Component.text;
import static net.kyori.adventure.text.LinearComponents.linear;
import static net.kyori.adventure.text.event.ClickEvent.openUrl;
import static net.kyori.adventure.text.event.ClickEvent.runCommand;
import static net.kyori.adventure.text.format.NamedTextColor.GRAY;
import static net.kyori.adventure.text.format.NamedTextColor.WHITE;
import static net.kyori.adventure.text.format.TextColor.color;
import static net.kyori.adventure.text.format.TextDecoration.STRIKETHROUGH;
public final class CommandHandlerFactory {
private final MiniMOTD<?> miniMOTD;
public CommandHandlerFactory(final @NonNull MiniMOTD<?> miniMOTD) {
this.miniMOTD = miniMOTD;
}
public @NonNull CommandHandler about() {
return audience -> {
final Component header = miniMessage("<gradient:white:#007FFF:white> ").decorate(STRIKETHROUGH);
Stream.of(
header,
text()
.hoverEvent(miniMessage("<rainbow>click me!"))
.clickEvent(openUrl(Constants.PluginMetadata.WEBSITE))
.content(Constants.PluginMetadata.NAME)
.color(WHITE)
.append(space())
.append(miniMessage("<gradient:#0047AB:#007FFF>" + Constants.PluginMetadata.VERSION))
.build(),
text()
.content("By ")
.color(GRAY)
.append(text("jmp", WHITE))
.build(),
header
).forEach(audience::sendMessage);
};
}
public @NonNull CommandHandler reload() {
return audience -> {
this.miniMOTD.reload();
audience.sendMessage(linear(Constants.COMMAND_PREFIX, space(), text("Done reloading configuration.", NamedTextColor.GREEN)));
};
}
public @NonNull CommandHandler help() {
return audience -> Stream.of(
linear(Constants.COMMAND_PREFIX, space(), text(Constants.PluginMetadata.NAME + " command help", WHITE)),
commandInfo("/minimotd about", "Show information about MiniMOTD"),
commandInfo("/minimotd reload", "Reload MiniMOTD configuration files"),
commandInfo("/minimotd help", "Show this help menu")
).forEach(audience::sendMessage);
}
private static @NonNull Component commandInfo(final @NonNull String command, final @NonNull String description) {
return text()
.content(" - ")
.color(GRAY)
.append(
text(command, color(0x007FFF))
.replaceText(config -> {
config.matchLiteral("/");
config.replacement(builder -> builder.color(WHITE));
})
)
.append(text(':', GRAY))
.append(space())
.append(text(description, WHITE))
.hoverEvent(
text()
.content("Click to execute '")
.color(GRAY)
.append(text(command, WHITE))
.append(text("'"))
.build()
)
.clickEvent(runCommand(command))
.build();
}
private static @NonNull Component miniMessage(final @NonNull String message) {
return MiniMessage.get().parse(message);
}
@FunctionalInterface
public interface CommandHandler {
void execute(@NonNull Audience audience);
}
}
@@ -0,0 +1,50 @@
/*
* This file is part of MiniMOTD, licensed under the MIT License.
*
* Copyright (c) 2021 Jason Penilla
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package xyz.jpenilla.minimotd.common;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import org.checkerframework.checker.nullness.qual.NonNull;
public interface ComponentColorDownsampler {
static @NonNull ComponentColorDownsampler downsampler() {
return ComponentColorDownsamplerImpl.INSTANCE;
}
@NonNull Component downsample(@NonNull Component component);
final class ComponentColorDownsamplerImpl implements ComponentColorDownsampler {
private static final ComponentColorDownsampler INSTANCE = new ComponentColorDownsamplerImpl();
private ComponentColorDownsamplerImpl() {
}
@Override
public @NonNull Component downsample(final @NonNull Component component) {
return GsonComponentSerializer.gson().deserializeFromTree(
GsonComponentSerializer.colorDownsamplingGson().serializeToTree(component)
);
}
}
}
@@ -23,9 +23,23 @@
*/
package xyz.jpenilla.minimotd.common;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
public final class Constants {
private Constants() {
}
public static final class PluginMetadata {
private PluginMetadata() {
}
public static final String NAME = "${PLUGIN_NAME}";
public static final String VERSION = "${PLUGIN_VERSION}";
public static final String WEBSITE = "${PLUGIN_WEBSITE}";
}
public static final int MINECRAFT_1_16_PROTOCOL_VERSION = 735;
public static final Component COMMAND_PREFIX = MiniMessage.get().parse("<white>[</white><gradient:#0047AB:#ADD8E6>" + PluginMetadata.NAME + "<white>]</white>");
}
@@ -29,6 +29,9 @@ import org.checkerframework.checker.nullness.qual.Nullable;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
@@ -38,34 +41,41 @@ public final class IconManager<I> {
private final Map<String, I> icons = new HashMap<>();
private final MiniMOTD<I> miniMOTD;
private final File iconsDirectory;
private final Path iconsDirectory;
public IconManager(final @NonNull MiniMOTD<I> miniMOTD) {
this.miniMOTD = miniMOTD;
this.iconsDirectory = miniMOTD.dataDirectory().resolve("icons").toFile();
this.iconsDirectory = miniMOTD.dataDirectory().resolve("icons");
this.loadIcons();
}
public void loadIcons() {
if (!this.iconsDirectory.exists()) {
this.iconsDirectory.mkdir();
}
this.icons.clear();
final File[] icons = this.iconsDirectory.listFiles(i -> i.getName().endsWith(".png"));
if (icons != null) {
for (final File icon : icons) {
try {
final BufferedImage bufferedImage = ImageIO.read(icon);
if (bufferedImage.getHeight() == 64 && bufferedImage.getWidth() == 64) {
final I newIcon = this.miniMOTD.platform().loadIcon(bufferedImage);
this.icons.put(icon.getName().split("\\.")[0], newIcon);
} else {
this.miniMOTD.logger().warn("Could not load " + icon.getName() + ": image must be 64x64px");
}
} catch (final Exception e) {
this.miniMOTD.logger().warn("Could not load " + icon.getName() + ": invalid image file", e);
}
try {
if (!Files.exists(this.iconsDirectory)) {
Files.createDirectories(this.iconsDirectory);
}
Files.list(this.iconsDirectory)
.map(Path::toFile)
.filter(File::isFile)
.filter(file -> file.getName().endsWith(".png"))
.forEach(this::loadIcon);
} catch (final IOException ex) {
throw new RuntimeException("Exception loading server icons", ex);
}
}
private void loadIcon(final @NonNull File iconFile) {
try {
final BufferedImage bufferedImage = ImageIO.read(iconFile);
if (bufferedImage.getHeight() == 64 && bufferedImage.getWidth() == 64) {
final I newIcon = this.miniMOTD.platform().loadIcon(bufferedImage);
this.icons.put(iconFile.getName().split("\\.")[0], newIcon);
} else {
this.miniMOTD.logger().warn("Could not load " + iconFile.getName() + ": image must be 64x64px");
}
} catch (final Exception ex) {
this.miniMOTD.logger().warn("Could not load " + iconFile.getName() + ": invalid image file", ex);
}
}
@@ -23,14 +23,15 @@
*/
package xyz.jpenilla.minimotd.common;
import net.kyori.adventure.text.Component;
import org.checkerframework.checker.nullness.qual.Nullable;
public final class MOTDIconPair<I> {
private final I icon;
private final String motd;
private final Component motd;
public MOTDIconPair(final @Nullable I icon, final @Nullable String motd) {
public MOTDIconPair(final @Nullable I icon, final @Nullable Component motd) {
this.icon = icon;
this.motd = motd;
}
@@ -39,7 +40,7 @@ public final class MOTDIconPair<I> {
return this.icon;
}
public @Nullable String motd() {
public @Nullable Component motd() {
return this.motd;
}
@@ -23,6 +23,9 @@
*/
package xyz.jpenilla.minimotd.common;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.slf4j.Logger;
import xyz.jpenilla.minimotd.common.config.ConfigManager;
@@ -63,19 +66,22 @@ public final class MiniMOTD<I> {
return this.configManager;
}
// todo: move deserialization from MM -> Component, color down-sampling here, take protocolVersion int as param
public final @NonNull MOTDIconPair<I> createMOTD(final @NonNull MiniMOTDConfig config, final int onlinePlayers, final int maxPlayers) {
I icon = null;
String motd = null;
Component motd = null;
String iconString = null;
if (config.motdEnabled()) {
if (config.motds().size() == 0) {
throw new IllegalStateException("MOTD is enabled, but there are no MOTDs in the config file?");
}
final int index = config.motds().size() == 1 ? 0 : ThreadLocalRandom.current().nextInt(config.motds().size());
final MiniMOTDConfig.MOTD m = config.motds().get(index);
motd = String.format("%s<reset>\n%s", m.line1(), m.line2())
.replace("{onlinePlayers}", String.valueOf(onlinePlayers))
.replace("{maxPlayers}", String.valueOf(maxPlayers))
.replace("{br}", "\n");
iconString = m.icon();
final MiniMOTDConfig.MOTD motdConfig = config.motds().get(index);
motd = TextComponent.ofChildren(
MiniMessage.get().parse(replacePlayerCount(motdConfig.line1(), onlinePlayers, maxPlayers)),
Component.newline(),
MiniMessage.get().parse(replacePlayerCount(motdConfig.line2(), onlinePlayers, maxPlayers))
);
iconString = motdConfig.icon();
}
if (config.iconEnabled()) {
icon = this.iconManager().icon(iconString);
@@ -83,29 +89,14 @@ public final class MiniMOTD<I> {
return new MOTDIconPair<>(icon, motd);
}
public final int calculateOnlinePlayers(final @NonNull MiniMOTDConfig config, final int realOnlinePlayers) {
if (config.fakePlayersEnabled()) {
try {
final String fakePlayersConfigString = config.fakePlayers();
if (fakePlayersConfigString.contains(":")) {
final String[] fakePlayers = fakePlayersConfigString.split(":");
final int start = Integer.parseInt(fakePlayers[0]);
final int end = Integer.parseInt(fakePlayers[1]);
private static @NonNull String replacePlayerCount(final @NonNull String input, final int onlinePlayers, final int maxPlayers) {
return input.replace("{onlinePlayers}", String.valueOf(onlinePlayers))
.replace("{maxPlayers}", String.valueOf(maxPlayers));
}
return realOnlinePlayers + ThreadLocalRandom.current().nextInt(start, end);
} else if (fakePlayersConfigString.contains("%")) {
final double factor = 1 + (Double.parseDouble(fakePlayersConfigString.replace("%", "")) / 100);
return (int) Math.ceil(factor * realOnlinePlayers);
} else {
final int addedPlayers = Integer.parseInt(fakePlayersConfigString);
return realOnlinePlayers + addedPlayers;
}
} catch (final NumberFormatException ex) {
this.logger().warn("fakePlayers config incorrect");
}
}
return realOnlinePlayers;
public void reload() {
this.iconManager.loadIcons();
this.configManager.loadConfigs();
this.platform.onReload();
}
}
@@ -35,4 +35,7 @@ public interface MiniMOTDPlatform<I> {
@NonNull Logger logger();
@NonNull I loadIcon(@NonNull BufferedImage image) throws Exception;
default void onReload() {
}
}
@@ -24,6 +24,7 @@
package xyz.jpenilla.minimotd.common;
import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableList;
import com.google.gson.JsonArray;
import com.google.gson.JsonParser;
import org.checkerframework.checker.nullness.qual.NonNull;
@@ -31,7 +32,7 @@ import org.checkerframework.checker.nullness.qual.NonNull;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
@@ -39,39 +40,33 @@ import java.util.Map;
public final class UpdateChecker {
private final JsonParser parser = new JsonParser();
private final String currentVersion;
public UpdateChecker(final @NonNull String currentVersion) {
this.currentVersion = currentVersion;
}
public List<String> checkVersion() {
final List<String> messages = new ArrayList<>();
public @NonNull List<String> checkVersion() {
final JsonArray result;
try {
result = this.parser.parse(new InputStreamReader(new URL("https://api.github.com/repos/jpenilla/MiniMOTD/releases").openStream(), Charsets.UTF_8)).getAsJsonArray();
} catch (final IOException exception) {
messages.add("Cannot look for updates: " + exception.getMessage());
return messages;
return Collections.singletonList("Cannot look for updates: " + exception.getMessage());
}
final Map<String, String> versionMap = new LinkedHashMap<>();
result.forEach(element -> versionMap.put(element.getAsJsonObject().get("tag_name").getAsString(), element.getAsJsonObject().get("html_url").getAsString()));
final List<String> versionList = new LinkedList<>(versionMap.keySet());
final String currentVersion = "v" + this.currentVersion;
final String currentVersion = "v" + Constants.PluginMetadata.VERSION;
if (versionList.get(0).equals(currentVersion)) {
messages.add("You are running the latest version of MiniMOTD! :)");
return messages;
return Collections.emptyList(); // Up to date, do nothing
}
if (currentVersion.contains("SNAPSHOT")) {
messages.add("You are running a development build of MiniMOTD! (" + currentVersion + ")");
messages.add("The latest official release is " + versionList.get(0));
return messages;
return ImmutableList.of(
"This server is running a development build of MiniMOTD! (" + currentVersion + ")",
"The latest official release is " + versionList.get(0)
);
}
final int versionsBehind = versionList.indexOf(currentVersion);
messages.add("There is an update available for MiniMOTD!");
messages.add("You are running version " + currentVersion + ", which is " + (versionsBehind == -1 ? "many" : versionsBehind) + " versions outdated.");
messages.add("Download the latest version, " + versionList.get(0) + " from GitHub at the link below:");
messages.add(versionMap.get(versionList.get(0)));
return messages;
return ImmutableList.of(
"There is an update available for " + Constants.PluginMetadata.NAME + "!",
"This server is running version " + currentVersion + ", which is " + (versionsBehind == -1 ? "UNKNOWN" : versionsBehind) + " versions outdated.",
"Download the latest version, " + versionList.get(0) + " from GitHub at the link below:",
versionMap.get(versionList.get(0))
);
}
}
@@ -39,7 +39,10 @@ public final class ConfigLoader<C> {
private static final TypeSerializerCollection SERIALIZERS;
static {
SERIALIZERS = TypeSerializerCollection.defaults();
SERIALIZERS = TypeSerializerCollection.defaults()
.childBuilder()
.register(PlayerCountModifier.serializer())
.build();
}
private final HoconConfigurationLoader loader;
@@ -27,7 +27,7 @@ import com.google.common.collect.ImmutableList;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.spongepowered.configurate.ConfigurateException;
import xyz.jpenilla.minimotd.common.MiniMOTD;
import xyz.jpenilla.minimotd.common.Pair;
import xyz.jpenilla.minimotd.common.util.Pair;
import java.io.IOException;
import java.nio.file.Files;
@@ -81,7 +81,7 @@ public final class ConfigManager {
try {
if (!Files.exists(extraConfigsDir)) {
Files.createDirectories(extraConfigsDir);
this.createDefaultProxyConfigs(extraConfigsDir);
this.createDefaultExtraConfigs(extraConfigsDir);
}
for (final Path path : Files.list(extraConfigsDir).collect(Collectors.toList())) {
if (path.toString().endsWith(".conf")) {
@@ -101,7 +101,7 @@ public final class ConfigManager {
}
}
private void createDefaultProxyConfigs(final @NonNull Path extraConfigsDir) throws ConfigurateException {
private void createDefaultExtraConfigs(final @NonNull Path extraConfigsDir) throws ConfigurateException {
final List<Pair<Path, MiniMOTDConfig.MOTD>> defaults = ImmutableList.of(
Pair.of(extraConfigsDir.resolve("skyblock.conf"), new MiniMOTDConfig.MOTD("<green><italic>Skyblock</green>", "<bold><rainbow>MiniMOTD Skyblock Server")),
Pair.of(extraConfigsDir.resolve("survival.conf"), new MiniMOTDConfig.MOTD("<gradient:blue:red>Survival Mode Hardcore", "<green><bold>MiniMOTD Survival Server"))
@@ -111,7 +111,7 @@ public final class ConfigManager {
MiniMOTDConfig.class,
pair.left()
);
loader.save(new MiniMOTDConfig(ImmutableList.of(pair.right())));
loader.save(new MiniMOTDConfig(pair.right()));
}
}
@@ -35,14 +35,14 @@ import java.util.List;
public final class MiniMOTDConfig {
public MiniMOTDConfig() {
this(Arrays.asList(
this(
new MOTD(),
new MOTD("<blue>Another <bold><red>MOTD", "<italic><underlined><gradient:red:green>much wow")
));
);
}
public MiniMOTDConfig(final @NonNull List<MOTD> defaultMOTDs) {
this.motds.addAll(defaultMOTDs);
public MiniMOTDConfig(final @NonNull MOTD @NonNull ... defaults) {
this.motds.addAll(Arrays.asList(defaults));
}
@Comment("The list of MOTDs to display\n"
@@ -59,14 +59,6 @@ public final class MiniMOTDConfig {
private PlayerCountSettings playerCountSettings = new PlayerCountSettings();
public int adjustedMaxPlayers(final int onlinePlayers, final int actualMaxPlayers) {
if (this.playerCountSettings.maxPlayersEnabled) {
return this.playerCountSettings.justXMoreSettings.justXMoreEnabled ? onlinePlayers + this.playerCountSettings.justXMoreSettings.xValue : this.playerCountSettings.maxPlayers;
} else {
return actualMaxPlayers;
}
}
@ConfigSerializable
public static final class MOTD {
@@ -114,6 +106,10 @@ public final class MiniMOTDConfig {
@Comment("Setting this to true will disable the hover text showing online player usernames")
private boolean disablePlayerListHover = false;
@Comment("Setting this to true will disable the player list hover (same as 'disable-player-list-hover'),\n"
+ "but will also cause the player count to appear as '???'")
private boolean hidePlayerCount = false;
@Comment("Settings for the fake player count feature")
private FakePlayers fakePlayers = new FakePlayers();
@@ -121,6 +117,10 @@ public final class MiniMOTDConfig {
+ "ex: x=3 -> 16/19 players online.")
private JustXMore justXMoreSettings = new JustXMore();
@Comment("Should the displayed online player count be allowed to exceed the displayed maximum player count?\n"
+ "If false, the online player count will be capped at the maximum player count")
private boolean allowExceedingMaximum = false;
@ConfigSerializable
public static final class JustXMore {
@@ -137,15 +137,19 @@ public final class MiniMOTDConfig {
@Comment("Enable fake player count feature")
private boolean fakePlayersEnabled = false;
@Comment("Modes: static, random, percent\n"
@Comment("Modes: add, constant, minimum, random, percent\n"
+ "\n"
+ " - static: This many fake players will be added\n"
+ " ex: fakePlayers: \"3\"\n"
+ " - add: This many fake players will be added\n"
+ " ex: fake-players=\"3\"\n"
+ " - constant: A constant value for the player count\n"
+ " ex: fake-players=\"=42\"\n"
+ " - minimum: The minimum bound of the player count\n"
+ " ex: fake-players=\"7+\"\n"
+ " - random: A random number of fake players in this range will be added\n"
+ " ex: fakePlayers: \"3:6\"\n"
+ " ex: fake-players=\"3:6\"\n"
+ " - percent: The player count will be inflated by this much, rounding up\n"
+ " ex: fakePlayers: \"25%\"")
private String fakePlayers = "25%";
+ " ex: fake-players=\"25%\"")
private PlayerCountModifier fakePlayers = PlayerCountModifier.parse("25%");
}
@@ -163,15 +167,59 @@ public final class MiniMOTDConfig {
return this.motdEnabled;
}
public boolean fakePlayersEnabled() {
return this.playerCountSettings.fakePlayers.fakePlayersEnabled;
}
public @NonNull String fakePlayers() {
return this.playerCountSettings.fakePlayers.fakePlayers;
}
public boolean disablePlayerListHover() {
return this.playerCountSettings.disablePlayerListHover;
}
public boolean hidePlayerCount() {
return this.playerCountSettings.hidePlayerCount;
}
private @NonNull PlayerCountModifier playerCountModifier() {
return this.playerCountSettings.fakePlayers.fakePlayers;
}
private int calculateOnlinePlayers(final int onlinePlayers) {
if (this.playerCountSettings.fakePlayers.fakePlayersEnabled) {
return this.playerCountModifier().apply(onlinePlayers);
}
return onlinePlayers;
}
private int calculateMaxPlayers(final int onlinePlayers, final int maxPlayers) {
if (this.playerCountSettings.maxPlayersEnabled) {
if (this.playerCountSettings.justXMoreSettings.justXMoreEnabled) {
return onlinePlayers + this.playerCountSettings.justXMoreSettings.xValue;
}
return this.playerCountSettings.maxPlayers;
}
return maxPlayers;
}
public @NonNull PlayerCount modifyPlayerCount(final int onlinePlayers, final int maxPlayers) {
final int online = this.calculateOnlinePlayers(onlinePlayers);
final int max = this.calculateMaxPlayers(online, maxPlayers);
if (!this.playerCountSettings.allowExceedingMaximum) {
return new PlayerCount(Math.min(online, max), max);
}
return new PlayerCount(online, max);
}
public static final class PlayerCount {
private final int onlinePlayers;
private final int maxPlayers;
private PlayerCount(final int onlinePlayers, final int maxPlayers) {
this.onlinePlayers = onlinePlayers;
this.maxPlayers = maxPlayers;
}
public int onlinePlayers() {
return this.onlinePlayers;
}
public int maxPlayers() {
return this.maxPlayers;
}
}
}
@@ -0,0 +1,131 @@
/*
* This file is part of MiniMOTD, licensed under the MIT License.
*
* Copyright (c) 2021 Jason Penilla
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package xyz.jpenilla.minimotd.common.config;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.spongepowered.configurate.serialize.ScalarSerializer;
import org.spongepowered.configurate.serialize.SerializationException;
import xyz.jpenilla.minimotd.common.util.Int2IntFunction;
import java.lang.reflect.Type;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.Predicate;
public final class PlayerCountModifier implements Int2IntFunction {
private final String input;
private final Int2IntFunction function;
private PlayerCountModifier(final @NonNull String input, final @NonNull Int2IntFunction function) {
this.input = input;
this.function = function;
}
@Override
public int apply(final int actualPlayers) {
return this.function.apply(actualPlayers);
}
public final @NonNull String input() {
return this.input;
}
public static @NonNull PlayerCountModifier parse(final @NonNull String input) {
try {
if (input.contains(":")) {
return parseRandomModifier(input);
} else if (input.contains("%")) {
return parsePercentModifier(input);
} else if (input.contains("=")) {
return parseConstantModifier(input);
} else if (input.contains("+")) {
return parseMinimumModifier(input);
} else {
return parseAddModifier(input);
}
} catch (final NumberFormatException ex) {
throw cannotParse(input, ex);
}
}
private static @NonNull PlayerCountModifier parseRandomModifier(final @NonNull String input) {
final String[] fakePlayers = input.split(":");
if (fakePlayers.length != 2) {
throw cannotParse(input, null);
}
final int start = Integer.parseInt(fakePlayers[0]);
final int end = Integer.parseInt(fakePlayers[1]);
return new PlayerCountModifier(input, actual -> actual + ThreadLocalRandom.current().nextInt(start, end));
}
private static @NonNull PlayerCountModifier parsePercentModifier(final @NonNull String input) {
final double factor = 1.00D + Double.parseDouble(input.replace("%", "")) / 100.00D;
return new PlayerCountModifier(input, actual -> (int) Math.ceil(factor * actual));
}
private static @NonNull PlayerCountModifier parseConstantModifier(final @NonNull String input) {
final int value = Integer.parseInt(input.replace("=", ""));
return new PlayerCountModifier(input, actual -> value);
}
private static @NonNull PlayerCountModifier parseMinimumModifier(final @NonNull String input) {
final int minPlayers = Integer.parseInt(input.replace("+", ""));
return new PlayerCountModifier(input, actual -> Math.max(minPlayers, actual));
}
private static @NonNull PlayerCountModifier parseAddModifier(final @NonNull String input) {
final int addedPlayers = Integer.parseInt(input);
return new PlayerCountModifier(input, actual -> actual + addedPlayers);
}
private static @NonNull IllegalArgumentException cannotParse(final @NonNull String input, final @Nullable Throwable cause) {
return new IllegalArgumentException(String.format("Unable to parse a player count modifier from input string '%s'.", input), cause);
}
public static @NonNull Serializer serializer() {
return Serializer.INSTANCE;
}
public static final class Serializer extends ScalarSerializer<PlayerCountModifier> {
private static final Serializer INSTANCE = new Serializer();
private Serializer() {
super(PlayerCountModifier.class);
}
@Override
public PlayerCountModifier deserialize(final @NonNull Type type, final @NonNull Object obj) throws SerializationException {
try {
return PlayerCountModifier.parse(obj.toString());
} catch (final IllegalArgumentException ex) {
throw new SerializationException(ex);
}
}
@Override
protected Object serialize(final @NonNull PlayerCountModifier item, final @NonNull Predicate<Class<?>> typeSupported) {
return item.input();
}
}
}
@@ -0,0 +1,29 @@
/*
* This file is part of MiniMOTD, licensed under the MIT License.
*
* Copyright (c) 2021 Jason Penilla
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package xyz.jpenilla.minimotd.common.util;
@FunctionalInterface
public interface Int2IntFunction {
int apply(int value);
}
@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package xyz.jpenilla.minimotd.common;
package xyz.jpenilla.minimotd.common.util;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
@@ -47,10 +47,15 @@ public final class Pair<L, R> {
@Override
public boolean equals(final @Nullable Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (this == o) {
return true;
}
if (o == null || this.getClass() != o.getClass()) {
return false;
}
final Pair<?, ?> pair = (Pair<?, ?>) o;
return this.left.equals(pair.left) && this.right.equals(pair.right);
return this.left.equals(pair.left)
&& this.right.equals(pair.right);
}
@Override
@@ -0,0 +1,65 @@
/*
* This file is part of MiniMOTD, licensed under the MIT License.
*
* Copyright (c) 2021 Jason Penilla
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package xyz.jpenilla.minimotd.common;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.format.TextColor;
import org.junit.jupiter.api.Test;
import static net.kyori.adventure.text.Component.text;
import static net.kyori.adventure.text.format.NamedTextColor.nearestTo;
import static org.junit.jupiter.api.Assertions.assertEquals;
class ComponentColorDownsamplerTest {
private static final TextColor GREEN = TextColor.color(0x04E762);
private static final TextColor ORANGE_YELLOW = TextColor.color(0xF5B700);
private static final TextColor PINK = TextColor.color(0xDC0073);
private static final TextColor BLUE = TextColor.color(0x008BF8);
private static final TextColor LIME_GREEN = TextColor.color(0x89FC00);
@Test
void testColorDownsampling() {
final Component fullColor = TextComponent.ofChildren(
text("Green ", GREEN),
text("Orange Yellow ", ORANGE_YELLOW),
text("Pink ", PINK),
text("Blue ", BLUE),
text("Lime Green", LIME_GREEN)
);
final Component expectedDownsample = TextComponent.ofChildren(
text("Green ", nearestTo(GREEN)),
text("Orange Yellow ", nearestTo(ORANGE_YELLOW)),
text("Pink ", nearestTo(PINK)),
text("Blue ", nearestTo(BLUE)),
text("Lime Green", nearestTo(LIME_GREEN))
);
final Component actualDownsample = ComponentColorDownsampler.downsampler().downsample(fullColor);
assertEquals(
expectedDownsample,
actualDownsample
);
}
}
+12 -16
View File
@@ -1,11 +1,8 @@
plugins {
id("fabric-loom") version "0.5-SNAPSHOT"
id("net.kyori.blossom") version "1.1.0"
id("fabric-loom") version "0.6-SNAPSHOT"
}
configurations {
create("shade")
}
val shade: Configuration by configurations.creating
val minecraftVersion = "1.16.5"
@@ -13,9 +10,9 @@ dependencies {
minecraft("com.mojang", "minecraft", minecraftVersion)
mappings(minecraft.officialMojangMappings())
modImplementation("net.fabricmc", "fabric-loader", "0.11.1")
modImplementation("net.fabricmc.fabric-api", "fabric-api", "0.29.4+1.16")
modImplementation("net.fabricmc.fabric-api", "fabric-api", "0.31.0+1.16")
add("shade", implementation(project(":minimotd-common")) {
shade(implementation(project(":minimotd-common")) {
exclude("net.kyori")
exclude("org.slf4j")
})
@@ -29,7 +26,7 @@ dependencies {
tasks {
shadowJar {
configurations = listOf(project.configurations.getByName("shade"))
configurations = listOf(shade)
relocate("io.leangen.geantyref", "xyz.jpenilla.minimotd.lib.io.leangen.geantyref")
relocate("org.spongepowered.configurate", "xyz.jpenilla.minimotd.lib.spongepowered.configurate")
relocate("com.typesafe.config", "xyz.jpenilla.minimotd.lib.typesafe.config")
@@ -40,20 +37,19 @@ tasks {
dependsOn(shadowJar)
input.set(shadowJar.get().outputs.files.singleFile)
archiveFileName.set("${project.name}-mc$minecraftVersion-${project.version}.jar")
destinationDirectory.set(rootProject.buildDir.resolve("libs"))
}
processResources {
filesMatching("fabric.mod.json") {
mapOf(
"{project.name}" to project.name,
"{rootProject.name}" to rootProject.name,
"{version}" to version.toString(),
"{description}" to project.description,
"{url}" to rootProject.ext["url"].toString()
).entries.forEach { (k, v) -> filter { it.replace(k, v as String) } }
"{project.version}" to project.version.toString(),
"{project.description}" to project.description.toString(),
"{project.github_url}" to rootProject.ext["url"].toString()
).forEach { (k, v) ->
filter { it.replace(k, v) }
}
}
}
}
blossom {
replaceToken("{version}", version.toString(), "src/main/java/xyz/jpenilla/minimotd/fabric/MiniMOTDFabric.java")
}
@@ -23,6 +23,8 @@
*/
package xyz.jpenilla.minimotd.fabric;
import com.mojang.brigadier.Command;
import com.mojang.brigadier.context.CommandContext;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufOutputStream;
import io.netty.buffer.Unpooled;
@@ -30,14 +32,12 @@ import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.command.v1.CommandRegistrationCallback;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.fabricmc.loader.api.FabricLoader;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.platform.fabric.FabricServerAudiences;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.minecraft.commands.Commands;
import org.apache.commons.lang3.Validate;
import net.minecraft.commands.CommandSourceStack;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import xyz.jpenilla.minimotd.common.CommandHandlerFactory;
import xyz.jpenilla.minimotd.common.MiniMOTD;
import xyz.jpenilla.minimotd.common.MiniMOTDPlatform;
import xyz.jpenilla.minimotd.common.UpdateChecker;
@@ -50,12 +50,13 @@ import java.nio.file.Path;
import java.util.Base64;
import java.util.concurrent.CompletableFuture;
public class MiniMOTDFabric implements ModInitializer, MiniMOTDPlatform<String> {
import static net.minecraft.commands.Commands.literal;
public final class MiniMOTDFabric implements ModInitializer, MiniMOTDPlatform<String> {
private static MiniMOTDFabric instance = null;
private final Logger logger = LoggerFactory.getLogger(MiniMOTD.class);
private final Path dataDirectory = FabricLoader.getInstance().getConfigDir().resolve("MiniMOTD");
private final MiniMessage miniMessage = MiniMessage.get();
private final MiniMOTD<String> miniMOTD = new MiniMOTD<>(this);
private FabricServerAudiences audiences;
@@ -71,54 +72,41 @@ public class MiniMOTDFabric implements ModInitializer, MiniMOTDPlatform<String>
return this.miniMOTD;
}
public @NonNull MiniMessage miniMessage() {
return this.miniMessage;
}
@Override
public void onInitialize() {
this.registerCommand();
ServerLifecycleEvents.SERVER_STARTED.register(minecraftServer -> {
this.audiences = FabricServerAudiences.of(minecraftServer);
if (this.miniMOTD.configManager().pluginSettings().updateChecker()) {
CompletableFuture.runAsync(() -> new UpdateChecker("{version}").checkVersion().forEach(this.logger()::info));
CompletableFuture.runAsync(() -> new UpdateChecker().checkVersion().forEach(this.logger()::info));
}
});
this.miniMOTD.logger().info("Done initializing MiniMOTD");
}
private void registerCommand() {
CommandRegistrationCallback.EVENT.register((dispatcher, dedicated) -> dispatcher.register(
Commands.literal("minimotd")
.requires(source -> source.hasPermission(4))
.then(Commands.literal("reload")
.executes(ctx -> {
final Audience audience = this.audiences.audience(ctx.getSource());
this.send(audience, "<white>[<gradient:blue:aqua>MiniMOTD</gradient>] <italic><gray>Reloading MiniMOTD...");
this.miniMOTD.configManager().loadConfigs();
this.miniMOTD.iconManager().loadIcons();
this.send(audience, "<white>[<gradient:blue:aqua>MiniMOTD</gradient>] <green>Done reloading MiniMOTD.");
return 1;
})
)
.then(Commands.literal("about")
.executes(ctx -> {
this.send(this.audiences.audience(ctx.getSource()),
"<strikethrough><gradient:black:white>------------------",
"<hover:show_text:'<gradient:blue:aqua>click me!'><click:open_url:https://github.com/jpenilla/MiniMOTD> MiniMOTD v{version}",
"<gray> By <gradient:gold:yellow>jmp",
"<strikethrough><gradient:black:white>------------------"
);
return 1;
})
)
));
}
final class WrappingExecutor implements Command<CommandSourceStack> {
private final CommandHandlerFactory.CommandHandler handler;
private void send(final @NonNull Audience audience, final String... strings) {
for (final String string : strings) {
audience.sendMessage(this.miniMessage.parse(string));
WrappingExecutor(final CommandHandlerFactory.@NonNull CommandHandler handler) {
this.handler = handler;
}
@Override
public int run(final @NonNull CommandContext<CommandSourceStack> context) {
this.handler.execute(MiniMOTDFabric.this.audiences.audience(context.getSource()));
return Command.SINGLE_SUCCESS;
}
}
final CommandHandlerFactory handlerFactory = new CommandHandlerFactory(this.miniMOTD);
CommandRegistrationCallback.EVENT.register((dispatcher, $) -> dispatcher.register(
literal("minimotd")
.requires(source -> source.hasPermission(4))
.then(literal("reload").executes(new WrappingExecutor(handlerFactory.reload())))
.then(literal("about").executes(new WrappingExecutor(handlerFactory.about())))
.then(literal("help").executes(new WrappingExecutor(handlerFactory.help())))
));
}
public @NonNull FabricServerAudiences audiences() {
@@ -141,14 +129,12 @@ public class MiniMOTDFabric implements ModInitializer, MiniMOTDPlatform<String>
@Override
public @NonNull String loadIcon(final @NonNull BufferedImage bufferedImage) throws Exception {
Validate.validState(bufferedImage.getWidth() == 64, "Must be 64 pixels wide");
Validate.validState(bufferedImage.getHeight() == 64, "Must be 64 pixels high");
final ByteBuf byteBuf = Unpooled.buffer();
final String icon;
try {
ImageIO.write(bufferedImage, "PNG", new ByteBufOutputStream(byteBuf));
final ByteBuffer byteBuffer = Base64.getEncoder().encode(byteBuf.nioBuffer());
icon = "data:image/png;base64," + StandardCharsets.UTF_8.decode(byteBuffer);
final ByteBuffer base64 = Base64.getEncoder().encode(byteBuf.nioBuffer());
icon = "data:image/png;base64," + StandardCharsets.UTF_8.decode(base64);
} finally {
byteBuf.release();
}
@@ -24,7 +24,7 @@
package xyz.jpenilla.minimotd.fabric.access;
public interface ConnectionAccess {
void minimotd$protocolVersion(int protocolVersion);
void protocolVersion(int protocolVersion);
int minimotd$protocolVersion();
int protocolVersion();
}
@@ -24,21 +24,24 @@
package xyz.jpenilla.minimotd.fabric.mixin;
import net.minecraft.network.Connection;
import org.spongepowered.asm.mixin.Implements;
import org.spongepowered.asm.mixin.Interface;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import xyz.jpenilla.minimotd.fabric.access.ConnectionAccess;
@SuppressWarnings("checkstyle:FinalLocalVariable")
@Unique
@Mixin(Connection.class)
abstract class ConnectionMixin implements ConnectionAccess {
@Unique
@Implements({@Interface(iface = ConnectionAccess.class, prefix = "tabtps$")})
abstract class ConnectionMixin {
private int protocolVersion = -1;
public void minimotd$protocolVersion(int protocolVersion) {
public void tabtps$protocolVersion(int protocolVersion) {
this.protocolVersion = protocolVersion;
}
public int minimotd$protocolVersion() {
public int tabtps$protocolVersion() {
return this.protocolVersion;
}
}
@@ -29,17 +29,19 @@ import net.minecraft.server.network.ServerHandshakePacketListenerImpl;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import xyz.jpenilla.minimotd.fabric.access.ConnectionAccess;
@Unique
@Mixin(ServerHandshakePacketListenerImpl.class)
abstract class ServerHandshakePacketListenerImplMixin {
@Shadow @Final private Connection connection;
@Inject(method = "handleIntention", at = @At("HEAD"))
public void onRequest(final ClientIntentionPacket packet, final CallbackInfo ci) {
((ConnectionAccess) this.connection).minimotd$protocolVersion(packet.getProtocolVersion());
public void injectHandleIntention(final ClientIntentionPacket packet, final CallbackInfo ci) {
((ConnectionAccess) this.connection).protocolVersion(packet.getProtocolVersion());
}
}
@@ -25,7 +25,6 @@ package xyz.jpenilla.minimotd.fabric.mixin;
import com.mojang.authlib.GameProfile;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.minecraft.network.Connection;
import net.minecraft.network.protocol.status.ServerStatus;
import net.minecraft.server.MinecraftServer;
@@ -33,47 +32,50 @@ import net.minecraft.server.network.ServerStatusPacketListenerImpl;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import xyz.jpenilla.minimotd.common.ComponentColorDownsampler;
import xyz.jpenilla.minimotd.common.Constants;
import xyz.jpenilla.minimotd.common.MOTDIconPair;
import xyz.jpenilla.minimotd.common.MiniMOTD;
import xyz.jpenilla.minimotd.common.config.MiniMOTDConfig;
import xyz.jpenilla.minimotd.common.config.MiniMOTDConfig.PlayerCount;
import xyz.jpenilla.minimotd.fabric.MiniMOTDFabric;
import xyz.jpenilla.minimotd.fabric.access.ConnectionAccess;
@Unique
@Mixin(ServerStatusPacketListenerImpl.class)
abstract class ServerStatusPacketListenerImplMixin {
@Shadow @Final private Connection connection;
@Redirect(method = "handleStatusRequest", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/MinecraftServer;getStatus()Lnet/minecraft/network/protocol/status/ServerStatus;"))
public ServerStatus method(final MinecraftServer minecraftServer) {
public ServerStatus injectHandleStatusRequest(final MinecraftServer minecraftServer) {
final ServerStatus vanillaStatus = minecraftServer.getStatus();
final ServerStatus modifiedStatus = new ServerStatus();
modifiedStatus.setDescription(vanillaStatus.getDescription());
modifiedStatus.setFavicon(vanillaStatus.getFavicon());
modifiedStatus.setPlayers(vanillaStatus.getPlayers());
modifiedStatus.setVersion(vanillaStatus.getVersion());
final MiniMOTDFabric miniMOTDFabric = MiniMOTDFabric.get();
final MiniMOTD<String> miniMOTD = miniMOTDFabric.miniMOTD();
final MiniMOTDConfig config = miniMOTD.configManager().mainConfig();
final int onlinePlayers = miniMOTD.calculateOnlinePlayers(config, minecraftServer.getPlayerCount());
final int maxPlayers = config.adjustedMaxPlayers(onlinePlayers, modifiedStatus.getPlayers().getMaxPlayers());
final PlayerCount count = config.modifyPlayerCount(minecraftServer.getPlayerCount(), vanillaStatus.getPlayers().getMaxPlayers());
final int onlinePlayers = count.onlinePlayers();
final int maxPlayers = count.maxPlayers();
final MOTDIconPair<String> pair = miniMOTD.createMOTD(config, onlinePlayers, maxPlayers);
final String motdString = pair.motd();
if (motdString != null) {
final Component motdComponent = miniMOTDFabric.miniMessage().parse(motdString);
if (((ConnectionAccess) this.connection).minimotd$protocolVersion() >= Constants.MINECRAFT_1_16_PROTOCOL_VERSION) {
final Component motdComponent = pair.motd();
if (motdComponent != null) {
if (((ConnectionAccess) this.connection).protocolVersion() >= Constants.MINECRAFT_1_16_PROTOCOL_VERSION) {
modifiedStatus.setDescription(miniMOTDFabric.audiences().toNative(motdComponent));
} else {
modifiedStatus.setDescription(net.minecraft.network.chat.Component.Serializer.fromJson(
GsonComponentSerializer.colorDownsamplingGson().serialize(motdComponent))
);
modifiedStatus.setDescription(miniMOTDFabric.audiences().toNative(
ComponentColorDownsampler.downsampler().downsample(motdComponent)
));
}
}
@@ -82,17 +84,17 @@ abstract class ServerStatusPacketListenerImplMixin {
modifiedStatus.setFavicon(favicon);
}
final GameProfile[] oldSample = modifiedStatus.getPlayers().getSample();
final ServerStatus.Players newPlayers = new ServerStatus.Players(maxPlayers, onlinePlayers);
if (config.disablePlayerListHover()) {
newPlayers.setSample(new GameProfile[]{});
} else {
newPlayers.setSample(oldSample);
if (!config.hidePlayerCount()) {
final GameProfile[] oldSample = vanillaStatus.getPlayers().getSample();
final ServerStatus.Players newPlayers = new ServerStatus.Players(maxPlayers, onlinePlayers);
if (config.disablePlayerListHover()) {
newPlayers.setSample(new GameProfile[]{});
} else {
newPlayers.setSample(oldSample);
}
modifiedStatus.setPlayers(newPlayers);
}
modifiedStatus.setPlayers(newPlayers);
return modifiedStatus;
}
}
+8 -3
View File
@@ -1,12 +1,17 @@
{
"schemaVersion": 1,
"id": "{project.name}",
"version": "{version}",
"version": "{project.version}",
"name": "{rootProject.name}",
"description": "{description}",
"description": "{project.description}",
"authors": [
"jmp"
],
"contact": {
"homepage": "{project.github_url}",
"sources": "{project.github_url}",
"issues": "{project.github_url}/issues"
},
"license": "MIT",
"environment": "server",
"entrypoints": {
@@ -19,7 +24,7 @@
],
"depends": {
"fabricloader": ">=0.11.1",
"fabric": "*",
"fabric": ">=0.31.0",
"minecraft": "1.16.x"
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+3
View File
@@ -17,6 +17,9 @@ setupSubproject("minimotd-spigot") {
setupSubproject("minimotd-sponge8") {
projectDir = file("sponge8")
}
setupSubproject("minimotd-sponge7") {
projectDir = file("sponge7")
}
setupSubproject("minimotd-bungeecord") {
projectDir = file("bungee")
}
+7 -7
View File
@@ -1,13 +1,13 @@
plugins {
id("kr.entree.spigradle") version "2.2.3"
id("net.minecrell.plugin-yml.bukkit") version "0.3.0"
}
dependencies {
implementation(project(":minimotd-common"))
implementation("org.slf4j", "slf4j-jdk14","1.7.30")
implementation("net.kyori", "adventure-platform-bukkit", "4.0.0-SNAPSHOT")
implementation("org.bstats", "bstats-bukkit", "2.2.0")
compileOnly("com.destroystokyo.paper", "paper-api", "1.16.4-R0.1-SNAPSHOT")
implementation("org.bstats", "bstats-bukkit", "2.2.1")
compileOnly("com.destroystokyo.paper", "paper-api", "1.16.5-R0.1-SNAPSHOT")
}
tasks {
@@ -25,16 +25,16 @@ tasks {
}
}
spigot {
bukkit {
name = rootProject.name
main = "xyz.jpenilla.minimotd.spigot.MiniMOTDPlugin"
apiVersion = "1.13"
website = rootProject.ext["url"].toString()
authors("jmp")
softDepends("Prisma", "ViaVersion")
authors = listOf("jmp")
softDepend = listOf("ViaVersion")
commands {
create("minimotd") {
description = "MiniMOTD Command"
permissionMessage = "No permission"
usage = "/minimotd help"
}
}
@@ -30,6 +30,7 @@ import org.bukkit.command.PluginCommand;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.util.CachedServerIcon;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import xyz.jpenilla.minimotd.common.MiniMOTD;
@@ -40,51 +41,45 @@ import java.awt.image.BufferedImage;
import java.nio.file.Path;
public final class MiniMOTDPlugin extends JavaPlugin implements MiniMOTDPlatform<CachedServerIcon> {
private static final boolean PAPER_PING_EVENT_EXISTS = findClass("com.destroystokyo.paper.event.server.PaperServerListPingEvent") != null;
private Logger logger;
private MiniMOTD<CachedServerIcon> miniMOTD;
private boolean isPaperServer;
private BukkitAudiences audiences;
private String serverPackageName;
private String serverApiVersion;
private int majorMinecraftVersion;
private BukkitAudiences audiences;
private Logger logger;
@Override
public void onEnable() {
this.logger = LoggerFactory.getLogger(this.getName());
this.miniMOTD = new MiniMOTD<>(this);
this.audiences = BukkitAudiences.create(this);
this.serverPackageName = this.getServer().getClass().getPackage().getName();
this.serverApiVersion = this.serverPackageName.substring(this.serverPackageName.lastIndexOf('.') + 1);
this.majorMinecraftVersion = Integer.parseInt(this.serverApiVersion.split("_")[1]);
try {
Class.forName("com.destroystokyo.paper.event.server.PaperServerListPingEvent");
this.isPaperServer = true;
} catch (final ClassNotFoundException e) {
this.isPaperServer = false;
}
if (this.isPaperServer) {
getServer().getPluginManager().registerEvents(new PaperPingListener(this, this.miniMOTD), this);
if (PAPER_PING_EVENT_EXISTS) {
Bukkit.getPluginManager().registerEvents(new PaperPingListener(this, this.miniMOTD), this);
} else {
getServer().getPluginManager().registerEvents(new PingListener(this, this.miniMOTD), this);
if (this.majorMinecraftVersion > 11) {
getLogger().info("#");
getLogger().info("# This server is not using Paper, and therefore some features may be limited or disabled.");
getLogger().info("# Get Paper from https://papermc.io/downloads");
getLogger().info("#");
Bukkit.getPluginManager().registerEvents(new PingListener(this, this.miniMOTD), this);
if (this.majorMinecraftVersion >= 12) { // PaperServerListPingEvent was added in 1.12
this.suggestPaper();
}
}
this.audiences = BukkitAudiences.create(this);
final PluginCommand command = getCommand("minimotd");
final PluginCommand command = this.getCommand("minimotd");
if (command != null) {
command.setExecutor(new SpigotCommand(this));
command.setTabCompleter(new SpigotCommand(this));
final SpigotCommand spigotCommand = new SpigotCommand(this);
command.setExecutor(spigotCommand);
command.setTabCompleter(spigotCommand);
}
final Metrics metrics = new Metrics(this, 8132);
if (this.miniMOTD.configManager().pluginSettings().updateChecker()) {
Bukkit.getScheduler().runTaskAsynchronously(this, () ->
new UpdateChecker(this.getDescription().getVersion()).checkVersion().forEach(getLogger()::info));
new UpdateChecker().checkVersion().forEach(this.logger::info));
}
}
@@ -114,4 +109,26 @@ public final class MiniMOTDPlugin extends JavaPlugin implements MiniMOTDPlatform
public @NonNull BukkitAudiences audiences() {
return this.audiences;
}
private void suggestPaper() {
this.logger.warn("======================================================");
this.logger.warn(" MiniMOTD works better if you use Paper as your server");
this.logger.warn(" software.");
this.logger.warn(" ");
this.logger.warn(" Spigot does not include the necessary APIs for all");
this.logger.warn(" of MiniMOTD's features to operate. MiniMOTD was");
this.logger.warn(" designed to work with Paper and it's expanded API");
this.logger.warn(" for full compatibility.");
this.logger.warn(" ");
this.logger.warn(" Get Paper from https://papermc.io/downloads");
this.logger.warn("======================================================");
}
private static @Nullable Class<?> findClass(final @NonNull String className) {
try {
return Class.forName(className);
} catch (final ClassNotFoundException ex) {
return null;
}
}
}
@@ -25,7 +25,6 @@ package xyz.jpenilla.minimotd.spigot;
import com.destroystokyo.paper.event.server.PaperServerListPingEvent;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
@@ -34,11 +33,11 @@ import org.checkerframework.checker.nullness.qual.NonNull;
import xyz.jpenilla.minimotd.common.MOTDIconPair;
import xyz.jpenilla.minimotd.common.MiniMOTD;
import xyz.jpenilla.minimotd.common.config.MiniMOTDConfig;
import xyz.jpenilla.minimotd.common.config.MiniMOTDConfig.PlayerCount;
public class PaperPingListener implements Listener {
private final MiniMOTDPlugin plugin;
private final LegacyComponentSerializer serializer = LegacyComponentSerializer.builder().hexColors().useUnusualXRepeatedCharacterHexFormat().build();
private final MiniMessage miniMessage = MiniMessage.get();
private final LegacyComponentSerializer legacySerializer = LegacyComponentSerializer.builder().build();
private final MiniMOTD<CachedServerIcon> miniMOTD;
@@ -50,17 +49,18 @@ public class PaperPingListener implements Listener {
@EventHandler
public void onPing(final @NonNull PaperServerListPingEvent e) {
final MiniMOTDConfig cfg = this.miniMOTD.configManager().mainConfig();
final int onlinePlayers = this.miniMOTD.calculateOnlinePlayers(cfg, e.getNumPlayers());
e.setNumPlayers(onlinePlayers);
final int maxPlayers = cfg.adjustedMaxPlayers(onlinePlayers, e.getMaxPlayers());
final PlayerCount count = cfg.modifyPlayerCount(e.getNumPlayers(), e.getMaxPlayers());
final int onlinePlayers = count.onlinePlayers();
final int maxPlayers = count.maxPlayers();
e.setNumPlayers(onlinePlayers);
e.setMaxPlayers(maxPlayers);
final MOTDIconPair<CachedServerIcon> pair = this.miniMOTD.createMOTD(cfg, onlinePlayers, maxPlayers);
final String motdString = pair.motd();
if (motdString != null) {
final Component motdComponent = this.miniMessage.parse(motdString);
final Component motdComponent = pair.motd();
if (motdComponent != null) {
if (e.getClient().getProtocolVersion() < 735 || this.plugin.majorMinecraftVersion() < 16) {
e.setMotd(this.legacySerializer.serialize(motdComponent));
} else {
@@ -76,5 +76,8 @@ public class PaperPingListener implements Listener {
if (cfg.disablePlayerListHover()) {
e.getPlayerSample().clear();
}
if (cfg.hidePlayerCount()) {
e.setHidePlayers(true);
}
}
}
@@ -23,7 +23,7 @@
*/
package xyz.jpenilla.minimotd.spigot;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
@@ -33,12 +33,12 @@ import org.checkerframework.checker.nullness.qual.NonNull;
import xyz.jpenilla.minimotd.common.MOTDIconPair;
import xyz.jpenilla.minimotd.common.MiniMOTD;
import xyz.jpenilla.minimotd.common.config.MiniMOTDConfig;
import xyz.jpenilla.minimotd.common.config.MiniMOTDConfig.PlayerCount;
public class PingListener implements Listener {
private final MiniMOTD<CachedServerIcon> miniMOTD;
private final LegacyComponentSerializer serializer = LegacyComponentSerializer.builder().hexColors().useUnusualXRepeatedCharacterHexFormat().build();
private final LegacyComponentSerializer legacySerializer = LegacyComponentSerializer.builder().build();
private final MiniMessage miniMessage = MiniMessage.get();
private final MiniMOTDPlugin plugin;
public PingListener(final @NonNull MiniMOTDPlugin plugin, final @NonNull MiniMOTD<CachedServerIcon> miniMOTD) {
@@ -50,18 +50,18 @@ public class PingListener implements Listener {
public void onPing(final @NonNull ServerListPingEvent e) {
final MiniMOTDConfig cfg = this.miniMOTD.configManager().mainConfig();
final int onlinePlayers = e.getNumPlayers();
final int actualMaxPlayers = e.getMaxPlayers();
final int maxPlayers = cfg.adjustedMaxPlayers(onlinePlayers, actualMaxPlayers);
final PlayerCount count = cfg.modifyPlayerCount(onlinePlayers, e.getMaxPlayers());
final int maxPlayers = count.maxPlayers();
e.setMaxPlayers(maxPlayers);
final MOTDIconPair<CachedServerIcon> pair = this.miniMOTD.createMOTD(cfg, onlinePlayers, maxPlayers);
final String motdString = pair.motd();
if (motdString != null) {
final Component motdComponent = pair.motd();
if (motdComponent != null) {
if (this.plugin.majorMinecraftVersion() > 15) {
e.setMotd(this.serializer.serialize(this.miniMessage.parse(motdString)));
e.setMotd(this.serializer.serialize(motdComponent));
} else {
e.setMotd(this.legacySerializer.serialize(this.miniMessage.parse(motdString)));
e.setMotd(this.legacySerializer.serialize(motdComponent));
}
}
@@ -24,112 +24,70 @@
package xyz.jpenilla.minimotd.spigot;
import com.google.common.collect.ImmutableList;
import net.kyori.adventure.identity.Identity;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.format.NamedTextColor;
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.checkerframework.checker.nullness.qual.NonNull;
import xyz.jpenilla.minimotd.common.CommandHandlerFactory;
import java.util.Collections;
import java.util.List;
public class SpigotCommand implements CommandExecutor, TabCompleter {
private final MiniMOTDPlugin plugin;
private final MiniMessage miniMessage;
private final CommandHandlerFactory handlerFactory;
public SpigotCommand(final @NonNull MiniMOTDPlugin plugin) {
this.plugin = plugin;
this.miniMessage = MiniMessage.get();
this.handlerFactory = new CommandHandlerFactory(plugin.miniMOTD());
}
@Override
public boolean onCommand(final @NonNull CommandSender sender,
final @NonNull Command command,
final @NonNull String label,
final @NonNull String[] args) {
public boolean onCommand(final @NonNull CommandSender sender, final @NonNull Command command, final @NonNull String label, final @NonNull String[] args) {
final Audience audience = this.plugin.audiences().sender(sender);
if (!sender.hasPermission("minimotd.admin")) {
this.onNoPermission(sender, args);
audience.sendMessage(Component.text("No permission.", NamedTextColor.RED));
return true;
}
if (args.length == 0) {
this.onInvalidUse(sender, args);
this.onInvalidUse(audience);
return true;
}
switch (args[0]) {
case "about":
this.onAbout(sender, args);
this.handlerFactory.about().execute(audience);
return true;
case "help":
this.onHelp(sender, args);
this.handlerFactory.help().execute(audience);
return true;
case "reload":
this.onReload(sender, args);
this.handlerFactory.reload().execute(audience);
return true;
}
this.onInvalidUse(sender, args);
this.onInvalidUse(audience);
return true;
}
private void onHelp(final @NonNull CommandSender sender, final @NonNull String[] args) {
this.send(sender, ImmutableList.of(
"<gradient:blue:green>MiniMOTD Commands:",
" <gray>-</gray> <hover:show_text:'<green>Click for <yellow>/minimotd about'><click:run_command:/minimotd about><yellow>/minimotd about",
" <gray>-</gray> <hover:show_text:'<green>Click for <yellow>/minimotd reload'><click:run_command:/minimotd reload><yellow>/minimotd reload",
" <gray>-</gray> <hover:show_text:'<green>Click for <yellow>/minimotd help'><click:run_command:/minimotd help><yellow>/minimotd help"
));
}
private void onReload(final @NonNull CommandSender sender, final @NonNull String[] args) {
this.plugin.miniMOTD().iconManager().loadIcons();
this.plugin.miniMOTD().configManager().loadConfigs();
this.send(sender, "<green>Done reloading.");
}
private void onAbout(final @NonNull CommandSender sender, final @NonNull String[] args) {
final String header = "<gradient:white:black>=============</gradient><gradient:black:white>=============";
this.send(sender, ImmutableList.of(
header,
"<hover:show_text:'<rainbow>click me!'><click:open_url:" + this.plugin.getDescription().getWebsite() + ">" + this.plugin.getName() + " <gradient:red:yellow>" + this.plugin.getDescription().getVersion(),
"<yellow>By</yellow><gray>:</gray> <gradient:blue:green>jmp",
header
));
}
private void onInvalidUse(final @NonNull CommandSender sender, final @NonNull String[] args) {
this.send(sender, "<hover:show_text:'<green>Click for <yellow>/minimotd help'><click:run_command:/minimotd help><italic><gradient:red:gold>Invalid usage.</gradient> <blue>Try <yellow>/minimotd help</yellow> or click here");
}
private void onNoPermission(final @NonNull CommandSender sender, final @NonNull String[] args) {
this.send(sender, "<gradient:red:yellow>No permission.");
}
private void send(final @NonNull CommandSender sender, final @NonNull String message) {
if (sender instanceof Player) {
this.plugin.audiences().player((Player) sender).sendMessage(Identity.nil(), this.miniMessage.parse(message));
} else {
this.plugin.audiences().console().sendMessage(Identity.nil(), this.miniMessage.parse(message));
}
}
private void send(final @NonNull CommandSender sender, final @NonNull List<String> messages) {
for (final String message : messages) {
this.send(sender, message);
}
private void onInvalidUse(final @NonNull Audience audience) {
audience.sendMessage(
Component.text("Invalid command usage. Use '/minimotd help' for a list of command provided by MiniMOTD.", NamedTextColor.RED)
.hoverEvent(Component.text("Click to execute '/minimotd help'"))
.clickEvent(ClickEvent.runCommand("/minimotd help"))
);
}
private static final List<String> COMMANDS = ImmutableList.of("about", "reload", "help");
@Override
public List<String> onTabComplete(final @NonNull CommandSender sender,
final @NonNull Command command,
final @NonNull String alias,
final @NonNull String[] args) {
public List<String> onTabComplete(final @NonNull CommandSender sender, final @NonNull Command command, final @NonNull String alias, final @NonNull String[] args) {
if (args.length < 2 && sender.hasPermission("minimotd.admin")) {
return COMMANDS;
}
+35
View File
@@ -0,0 +1,35 @@
dependencies {
implementation(project(":minimotd-common")) {
exclude("org.slf4j", "slf4j-api")
}
implementation("net.kyori", "adventure-platform-spongeapi", "4.0.0-SNAPSHOT")
compileOnly("org.spongepowered", "spongeapi", "7.2.0")
implementation("org.bstats", "bstats-sponge", "2.2.1")
}
tasks {
processResources {
filesMatching("mcmod.info") {
mapOf(
"{project.name}" to project.name,
"{rootProject.name}" to rootProject.name,
"{project.version}" to project.version.toString(),
"{project.description}" to project.description.toString(),
"{url}" to rootProject.ext["url"].toString()
).forEach { (k, v) ->
filter { resource -> resource.replace(k, v) }
}
}
}
shadowJar {
relocate("io.leangen.geantyref", "xyz.jpenilla.minimotd.lib.io.leangen.geantyref")
relocate("org.spongepowered.configurate", "xyz.jpenilla.minimotd.lib.spongepowered.configurate")
relocate("com.typesafe.config", "xyz.jpenilla.minimotd.lib.typesafe.config")
relocate("net.kyori", "xyz.jpenilla.minimotd.lib.kyori")
relocate("org.checkerframework", "xyz.jpenilla.minimotd.lib.checkerframework")
relocate("org.bstats", "xyz.jpenilla.minimotd.lib.bstats")
}
build {
dependsOn(shadowJar)
}
}
@@ -0,0 +1,88 @@
/*
* This file is part of MiniMOTD, licensed under the MIT License.
*
* Copyright (c) 2021 Jason Penilla
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package xyz.jpenilla.minimotd.sponge7;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.spongeapi.SpongeComponentSerializer;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.spongepowered.api.event.EventListener;
import org.spongepowered.api.event.server.ClientPingServerEvent;
import org.spongepowered.api.network.status.Favicon;
import xyz.jpenilla.minimotd.common.MOTDIconPair;
import xyz.jpenilla.minimotd.common.MiniMOTD;
import xyz.jpenilla.minimotd.common.config.MiniMOTDConfig;
import xyz.jpenilla.minimotd.common.config.MiniMOTDConfig.PlayerCount;
final class ClientPingServerEventListener implements EventListener<ClientPingServerEvent> {
private final MiniMOTD<Favicon> miniMOTD;
ClientPingServerEventListener(final @NonNull MiniMOTD<Favicon> miniMOTD) {
this.miniMOTD = miniMOTD;
}
@Override
public void handle(final @NonNull ClientPingServerEvent event) {
final ClientPingServerEvent.Response response = event.getResponse();
final ClientPingServerEvent.Response.Players players;
final ClientPingServerEvent.Response.Players players0 = response.getPlayers().orElse(null);
if (players0 != null) {
players = players0;
} else {
response.setHidePlayers(false);
players = response.getPlayers().orElse(null);
if (players == null) {
this.miniMOTD.logger().warn(String.format("Failed to handle ClientPingServerEvent: '%s', response.getPlayers() was null.", event));
return;
}
}
final MiniMOTDConfig config = this.miniMOTD.configManager().mainConfig();
final PlayerCount count = config.modifyPlayerCount(players.getOnline(), players.getMax());
final int onlinePlayers = count.onlinePlayers();
final int maxPlayers = count.maxPlayers();
players.setOnline(onlinePlayers);
players.setMax(maxPlayers);
final MOTDIconPair<Favicon> pair = this.miniMOTD.createMOTD(config, onlinePlayers, maxPlayers);
final Component motd = pair.motd();
if (motd != null) {
response.setDescription(SpongeComponentSerializer.get().serialize(motd));
}
final Favicon favicon = pair.icon();
if (favicon != null) {
response.setFavicon(favicon);
}
if (config.disablePlayerListHover()) {
players.getProfiles().clear();
}
if (config.hidePlayerCount()) {
response.setHidePlayers(true);
}
}
}
@@ -0,0 +1,143 @@
/*
* This file is part of MiniMOTD, licensed under the MIT License.
*
* Copyright (c) 2021 Jason Penilla
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package xyz.jpenilla.minimotd.sponge7;
import com.google.inject.Inject;
import net.kyori.adventure.platform.spongeapi.SpongeAudiences;
import org.bstats.sponge.Metrics;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.slf4j.Logger;
import org.spongepowered.api.Sponge;
import org.spongepowered.api.command.CommandResult;
import org.spongepowered.api.command.CommandSource;
import org.spongepowered.api.command.args.CommandContext;
import org.spongepowered.api.command.spec.CommandExecutor;
import org.spongepowered.api.command.spec.CommandSpec;
import org.spongepowered.api.config.ConfigDir;
import org.spongepowered.api.event.Listener;
import org.spongepowered.api.event.game.GameReloadEvent;
import org.spongepowered.api.event.game.state.GameStartedServerEvent;
import org.spongepowered.api.event.server.ClientPingServerEvent;
import org.spongepowered.api.network.status.Favicon;
import org.spongepowered.api.plugin.Plugin;
import org.spongepowered.api.scheduler.Task;
import xyz.jpenilla.minimotd.common.CommandHandlerFactory;
import xyz.jpenilla.minimotd.common.MiniMOTD;
import xyz.jpenilla.minimotd.common.MiniMOTDPlatform;
import xyz.jpenilla.minimotd.common.UpdateChecker;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.nio.file.Path;
@Plugin(id = "minimotd-sponge7")
public final class MiniMOTDPlugin implements MiniMOTDPlatform<Favicon> {
private final Logger logger;
private final Path dataDirectory;
private final MiniMOTD<Favicon> miniMOTD;
private final SpongeAudiences audiences;
@Inject
public MiniMOTDPlugin(
final @NonNull Logger logger,
@ConfigDir(sharedRoot = false) final @NonNull Path dataDirectory,
final @NonNull SpongeAudiences audiences,
final Metrics.@NonNull Factory metricsFactory
) {
this.logger = logger;
this.dataDirectory = dataDirectory;
this.audiences = audiences;
this.miniMOTD = new MiniMOTD<>(this);
metricsFactory.make(10768);
}
@Listener
public void gameStarted(final @NonNull GameStartedServerEvent event) {
Sponge.getEventManager().registerListener(this, ClientPingServerEvent.class, new ClientPingServerEventListener(this.miniMOTD));
this.registerCommands();
if (this.miniMOTD.configManager().pluginSettings().updateChecker()) {
Task.builder()
.async()
.execute(() -> new UpdateChecker().checkVersion().forEach(this.logger::info))
.submit(this);
}
}
@Listener
public void reloaded(final @NonNull GameReloadEvent event) {
this.miniMOTD.reload();
}
private void registerCommands() {
final class WrappingExecutor implements CommandExecutor {
private final CommandHandlerFactory.CommandHandler handler;
WrappingExecutor(final CommandHandlerFactory.@NonNull CommandHandler handler) {
this.handler = handler;
}
@Override
public CommandResult execute(final @NonNull CommandSource src, final @NonNull CommandContext args) {
this.handler.execute(MiniMOTDPlugin.this.audiences.receiver(src));
return CommandResult.success();
}
}
final CommandHandlerFactory factory = new CommandHandlerFactory(this.miniMOTD);
final CommandSpec help = CommandSpec.builder()
.executor(new WrappingExecutor(factory.help()))
.build();
final CommandSpec about = CommandSpec.builder()
.executor(new WrappingExecutor(factory.about()))
.build();
final CommandSpec reload = CommandSpec.builder()
.executor(new WrappingExecutor(factory.reload()))
.build();
Sponge.getCommandManager().register(
this,
CommandSpec.builder()
.permission("minimotd.admin")
.child(help, "help")
.child(about, "about")
.child(reload, "reload")
.build(),
"minimotd"
);
}
@Override
public @NonNull Path dataDirectory() {
return this.dataDirectory;
}
@Override
public @NonNull Logger logger() {
return this.logger;
}
@Override
public @NonNull Favicon loadIcon(final @NonNull BufferedImage image) throws IOException {
return Sponge.getRegistry().loadFavicon(image);
}
}
+18
View File
@@ -0,0 +1,18 @@
[
{
"modid": "{project.name}",
"name": "{rootProject.name}",
"version": "{project.version}",
"description": "{project.description}",
"url": "{url}",
"authorList": [
"jmp"
],
"dependencies": [
"spongeapi@7.2.0"
],
"requiredMods": [
"spongeapi@7.2.0"
]
}
]
+38 -16
View File
@@ -1,34 +1,56 @@
import org.spongepowered.gradle.plugin.config.PluginLoaders.JAVA_PLAIN
import org.spongepowered.plugin.metadata.PluginDependency.LoadOrder.AFTER
plugins {
id("org.spongepowered.gradle.plugin") version "1.0.2"
}
dependencies {
implementation(project(":minimotd-common"))
implementation("net.kyori", "adventure-text-serializer-legacy", "4.4.0")
compileOnly("org.spongepowered", "spongeapi", "8.0.0-SNAPSHOT")
annotationProcessor("org.spongepowered", "spongeapi", "8.0.0-SNAPSHOT")
implementation("org.apache.logging.log4j", "log4j-slf4j-impl", "2.8.1") {
isTransitive = false
}
}
sponge {
apiVersion("8.0.0")
plugin(project.name) {
loader(JAVA_PLAIN)
displayName(rootProject.name)
mainClass("xyz.jpenilla.minimotd.sponge8.MiniMOTDPlugin")
description(project.description)
links {
val url = rootProject.ext["url"].toString()
homepage(url)
source(url)
issues("${url}/issues")
}
contributor("jmp") {
description("Lead Developer")
}
dependency("spongeapi") {
loadOrder(AFTER)
optional(false)
}
}
}
tasks {
shadowJar {
relocate("org.apache.logging.slf4j", "xyz.jpenilla.minimotd.lib.apache.logging.slf4j")
relocate("org.slf4j", "xyz.jpenilla.minimotd.lib.slf4j_log4j")
relocate("org.spongepowered.configurate", "xyz.jpenilla.minimotd.lib.spongepowered.configurate")
relocate("io.leangen.geantyref", "xyz.jpenilla.minimotd.lib.io.leangen.geantyref")
relocate("com.typesafe.config", "xyz.jpenilla.minimotd.lib.typesafe.config")
relocate("net.kyori.adventure.text.minimessage", "xyz.jpenilla.minimotd.lib.kyori_native.minimessage")
relocate("net.kyori.adventure.text.serializer.legacy", "xyz.jpenilla.minimotd.lib.kyori_native.legacy.text.serializer")
relocate("org.checkerframework", "xyz.jpenilla.minimotd.lib.checkerframework")
relocate("xyz.jpenilla.minimotd.common", "xyz.jpenilla.minimotd.lib.sponge8.minimotd.common")
dependencies {
exclude { dep -> dep.moduleGroup == "net.kyori" && !dep.name.contains("minimessage") && !dep.name.contains("text-serializer-legacy") }
exclude(dependency("org.slf4j:slf4j-api"))
exclude { dep -> dep.moduleGroup == "net.kyori" && !dep.name.contains("minimessage") }
}
}
processResources {
filesMatching("**/plugins.json") {
mapOf(
"{project.name}" to project.name,
"{rootProject.name}" to rootProject.name,
"{version}" to version.toString(),
"{description}" to project.description,
"{url}" to rootProject.ext["url"].toString()
).entries.forEach { (k, v) -> filter { it.replace(k, v as String) } }
}
runServer {
classpath(shadowJar)
}
build {
dependsOn(shadowJar)
@@ -0,0 +1,111 @@
/*
* This file is part of MiniMOTD, licensed under the MIT License.
*
* Copyright (c) 2021 Jason Penilla
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package xyz.jpenilla.minimotd.sponge8;
import net.kyori.adventure.text.Component;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.spongepowered.api.MinecraftVersion;
import org.spongepowered.api.event.EventListener;
import org.spongepowered.api.event.server.ClientPingServerEvent;
import org.spongepowered.api.network.status.Favicon;
import xyz.jpenilla.minimotd.common.ComponentColorDownsampler;
import xyz.jpenilla.minimotd.common.Constants;
import xyz.jpenilla.minimotd.common.MOTDIconPair;
import xyz.jpenilla.minimotd.common.MiniMOTD;
import xyz.jpenilla.minimotd.common.config.MiniMOTDConfig;
import xyz.jpenilla.minimotd.common.config.MiniMOTDConfig.PlayerCount;
import java.lang.reflect.Method;
final class ClientPingServerEventListener implements EventListener<ClientPingServerEvent> {
private final MiniMOTD<Favicon> miniMOTD;
private @MonotonicNonNull Method SpongeMinecraftVersion_getProtocol;
ClientPingServerEventListener(final @NonNull MiniMOTD<Favicon> miniMOTD) {
this.miniMOTD = miniMOTD;
}
@Override
public void handle(final @NonNull ClientPingServerEvent event) {
final ClientPingServerEvent.Response response = event.response();
final ClientPingServerEvent.Response.Players players;
final ClientPingServerEvent.Response.Players players0 = response.players().orElse(null);
if (players0 != null) {
players = players0;
} else {
response.setHidePlayers(false);
players = response.players().orElse(null);
if (players == null) {
this.miniMOTD.logger().warn(String.format("Failed to handle ClientPingServerEvent: '%s', response.players() was null.", event));
return;
}
}
final MiniMOTDConfig config = this.miniMOTD.configManager().mainConfig();
final PlayerCount count = config.modifyPlayerCount(players.online(), players.max());
final int onlinePlayers = count.onlinePlayers();
final int maxPlayers = count.maxPlayers();
players.setOnline(onlinePlayers);
players.setMax(maxPlayers);
final MOTDIconPair<Favicon> pair = this.miniMOTD.createMOTD(config, onlinePlayers, maxPlayers);
final Component motdComponent = pair.motd();
if (motdComponent != null) {
final MinecraftVersion version = event.client().version();
if (this.legacy(version)) {
response.setDescription(ComponentColorDownsampler.downsampler().downsample(motdComponent));
} else {
response.setDescription(motdComponent);
}
}
final Favicon favicon = pair.icon();
if (favicon != null) {
response.setFavicon(favicon);
}
if (config.disablePlayerListHover()) {
players.profiles().clear();
}
if (config.hidePlayerCount()) {
response.setHidePlayers(true);
}
}
private boolean legacy(final @NonNull MinecraftVersion version) {
try {
if (!version.isLegacy() && this.SpongeMinecraftVersion_getProtocol == null) {
this.SpongeMinecraftVersion_getProtocol = version.getClass().getMethod("getProtocol");
}
return version.isLegacy()
|| (int) this.SpongeMinecraftVersion_getProtocol.invoke(version) < Constants.MINECRAFT_1_16_PROTOCOL_VERSION;
} catch (final ReflectiveOperationException e) {
throw new IllegalStateException("Failed to get protocol version", e);
}
}
}
@@ -24,41 +24,38 @@
package xyz.jpenilla.minimotd.sponge8;
import com.google.inject.Inject;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongepowered.api.MinecraftVersion;
import org.spongepowered.api.Sponge;
import org.spongepowered.api.command.Command;
import org.spongepowered.api.command.CommandExecutor;
import org.spongepowered.api.command.CommandResult;
import org.spongepowered.api.command.parameter.CommandContext;
import org.spongepowered.api.config.ConfigDir;
import org.spongepowered.api.event.Listener;
import org.spongepowered.api.event.lifecycle.LoadedGameEvent;
import org.spongepowered.api.event.lifecycle.RefreshGameEvent;
import org.spongepowered.api.event.lifecycle.RegisterCommandEvent;
import org.spongepowered.api.event.server.ClientPingServerEvent;
import org.spongepowered.api.network.status.Favicon;
import org.spongepowered.api.scheduler.Task;
import org.spongepowered.plugin.PluginContainer;
import org.spongepowered.plugin.jvm.Plugin;
import org.spongepowered.plugin.metadata.PluginMetadata;
import xyz.jpenilla.minimotd.common.Constants;
import xyz.jpenilla.minimotd.common.MOTDIconPair;
import xyz.jpenilla.minimotd.common.CommandHandlerFactory;
import xyz.jpenilla.minimotd.common.MiniMOTD;
import xyz.jpenilla.minimotd.common.MiniMOTDPlatform;
import xyz.jpenilla.minimotd.common.UpdateChecker;
import xyz.jpenilla.minimotd.common.config.MiniMOTDConfig;
import java.awt.image.BufferedImage;
import java.lang.reflect.Method;
import java.nio.file.Path;
import java.util.Objects;
@Plugin("minimotd-sponge8") // todo commands
public class MiniMOTDPlugin implements MiniMOTDPlatform<Favicon> {
@Plugin("minimotd-sponge8")
public final class MiniMOTDPlugin implements MiniMOTDPlatform<Favicon> {
private final Path dataDirectory;
private final PluginMetadata pluginMetadata;
private final Logger logger;
private final MiniMessage miniMessage = MiniMessage.get();
private final PluginContainer pluginContainer;
private final MiniMOTD<Favicon> miniMOTD;
@@ -72,58 +69,64 @@ public class MiniMOTDPlugin implements MiniMOTDPlatform<Favicon> {
this.pluginMetadata = pluginContainer.getMetadata();
this.logger = LoggerFactory.getLogger(this.pluginMetadata.getId());
this.miniMOTD = new MiniMOTD<>(this);
Sponge.eventManager().registerListener(
pluginContainer,
ClientPingServerEvent.class,
new ClientPingServerEventListener(this.miniMOTD)
);
}
@Listener
public void onServerStart(final @NonNull LoadedGameEvent event) {
Sponge.getAsyncScheduler().submit(Task.builder().plugin(this.pluginContainer).execute(() ->
new UpdateChecker(this.pluginMetadata.getVersion()).checkVersion().forEach(this.logger::info)
).build());
public void onGameLoaded(final @NonNull LoadedGameEvent event) {
if (this.miniMOTD.configManager().pluginSettings().updateChecker()) {
Sponge.asyncScheduler().submit(Task.builder()
.plugin(this.pluginContainer)
.execute(() -> new UpdateChecker().checkVersion().forEach(this.logger::info))
.build());
}
}
private Method SpongeMinecraftVersion_getProtocol;
@Listener
public void registerCommands(final @NonNull RegisterCommandEvent<Command.Parameterized> event) {
final class WrappingExecutor implements CommandExecutor {
private final CommandHandlerFactory.CommandHandler handler;
WrappingExecutor(final CommandHandlerFactory.@NonNull CommandHandler handler) {
this.handler = handler;
}
@Override
public CommandResult execute(final @NonNull CommandContext context) {
this.handler.execute(context.cause().audience());
return CommandResult.success();
}
}
final CommandHandlerFactory handlerFactory = new CommandHandlerFactory(this.miniMOTD);
final Command.Parameterized about = Command.builder()
.executor(new WrappingExecutor(handlerFactory.about()))
.build();
final Command.Parameterized help = Command.builder()
.executor(new WrappingExecutor(handlerFactory.help()))
.build();
final Command.Parameterized reload = Command.builder()
.executor(new WrappingExecutor(handlerFactory.reload()))
.build();
event.register(
this.pluginContainer,
Command.builder()
.permission("minimotd.admin")
.addChild(about, "about")
.addChild(help, "help")
.addChild(reload, "reload")
.build(),
"minimotd"
);
}
@Listener
public void onPing(final @NonNull ClientPingServerEvent event) {
final ClientPingServerEvent.Response response = event.getResponse();
response.getPlayers().ifPresent(players -> {
final MiniMOTDConfig config = this.miniMOTD.configManager().mainConfig();
final int onlinePlayers = this.miniMOTD.calculateOnlinePlayers(config, players.getOnline());
players.setOnline(onlinePlayers);
final int maxPlayers = config.adjustedMaxPlayers(onlinePlayers, players.getMax());
players.setMax(maxPlayers);
final @NonNull MOTDIconPair<Favicon> pair = this.miniMOTD.createMOTD(config, onlinePlayers, maxPlayers);
final String motdString = pair.motd();
try {
if (motdString != null) {
Component motdComponent = this.miniMessage.parse(motdString);
final MinecraftVersion version = event.getClient().getVersion();
if (!version.isLegacy() && this.SpongeMinecraftVersion_getProtocol == null) {
this.SpongeMinecraftVersion_getProtocol = version.getClass().getMethod("getProtocol");
}
if (version.isLegacy() || (int) this.SpongeMinecraftVersion_getProtocol.invoke(version) < Constants.MINECRAFT_1_16_PROTOCOL_VERSION) {
motdComponent = GsonComponentSerializer.colorDownsamplingGson().deserialize(GsonComponentSerializer.colorDownsamplingGson().serialize(motdComponent));
}
response.setDescription(motdComponent);
}
} catch (final ReflectiveOperationException e) {
throw new IllegalStateException("Failed to get protocol version", e);
}
final Favicon favicon = pair.icon();
if (favicon != null) {
response.setFavicon(favicon);
}
if (config.disablePlayerListHover()) {
players.getProfiles().clear();
}
});
public void onRefresh(final @NonNull RefreshGameEvent event) {
this.miniMOTD.reload();
}
@Override
@@ -138,6 +141,6 @@ public class MiniMOTDPlugin implements MiniMOTDPlatform<Favicon> {
@Override
public @NonNull Favicon loadIcon(final @NonNull BufferedImage image) throws Exception {
return Objects.requireNonNull(Favicon.load(image), "failed to load favicon");
return Favicon.load(image);
}
}
@@ -1,29 +0,0 @@
{
"plugins": [
{
"loader": "java_plain",
"id": "{project.name}",
"name": "{rootProject.name}",
"version": "{version}",
"main-class": "xyz.jpenilla.minimotd.sponge8.MiniMOTDPlugin",
"description": "{description}",
"links": {
"homepage": "{url}",
"source": "{url}",
"issues": "{url}issues"
},
"contributors": [
{
"name": "jmp",
"description": "Lead Developer"
}
],
"dependencies": [
{
"id": "spongeapi",
"version": "8.0.0"
}
]
}
]
}
+2 -1
View File
@@ -2,8 +2,9 @@ tasks {
val universal = register<Jar>("universal") {
artifacts.add("archives", this)
archiveClassifier.set("")
destinationDirectory.set(rootProject.buildDir.resolve("libs"))
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
arrayOf("spigot", "bungeecord", "velocity", "sponge8").forEach {
arrayOf("spigot", "bungeecord", "velocity", "sponge7", "sponge8").forEach {
val shadowJar = rootProject.project(":minimotd-$it").tasks.getByName("shadowJar")
from(zipTree(shadowJar.outputs.files.singleFile))
dependsOn(shadowJar)
+8 -16
View File
@@ -1,14 +1,12 @@
import net.kyori.blossom.BlossomExtension
plugins {
id("net.kyori.blossom") version "1.1.0"
id("net.kyori.blossom")
}
dependencies {
implementation(project(":minimotd-common")) {
exclude("org.slf4j", "slf4j-api")
}
implementation("org.bstats", "bstats-velocity", "2.2.0")
implementation("org.bstats", "bstats-velocity", "2.2.1")
compileOnly("com.velocitypowered", "velocity-api", "1.1.4")
annotationProcessor("com.velocitypowered", "velocity-api", "1.1.4")
}
@@ -32,19 +30,13 @@ tasks {
}
blossom {
fun replaceTokens(file: String, vararg tokens: Pair<String, String>) = tokens.forEach { (k, v) ->
replaceToken("\${$k}", v, file)
}
replaceTokens(
"src/main/java/xyz/jpenilla/minimotd/velocity/MiniMOTDPlugin.java",
"{project.name}" to project.name,
"{rootProject.name}" to rootProject.name,
"{version}" to version.toString(),
"{description}" to description!!,
"{url}" to rootProject.ext["url"].toString()
"project.name" to project.name,
"description" to description!!,
"url" to rootProject.ext["url"].toString()
)
}
fun BlossomExtension.replaceTokens(
filePath: String,
vararg replacements: Pair<String, String>
) {
for ((token, replacement) in replacements) replaceToken(token, replacement, filePath)
}
@@ -24,47 +24,48 @@
package xyz.jpenilla.minimotd.velocity;
import com.google.inject.Inject;
import com.mojang.brigadier.Command;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.velocitypowered.api.command.BrigadierCommand;
import com.velocitypowered.api.command.CommandManager;
import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
import com.velocitypowered.api.event.proxy.ProxyPingEvent;
import com.velocitypowered.api.plugin.Plugin;
import com.velocitypowered.api.plugin.PluginContainer;
import com.velocitypowered.api.plugin.PluginDescription;
import com.velocitypowered.api.plugin.annotation.DataDirectory;
import com.velocitypowered.api.proxy.ProxyServer;
import com.velocitypowered.api.proxy.server.ServerPing;
import com.velocitypowered.api.util.Favicon;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import org.bstats.velocity.Metrics;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.slf4j.Logger;
import xyz.jpenilla.minimotd.common.CommandHandlerFactory;
import xyz.jpenilla.minimotd.common.Constants;
import xyz.jpenilla.minimotd.common.MOTDIconPair;
import xyz.jpenilla.minimotd.common.MiniMOTD;
import xyz.jpenilla.minimotd.common.MiniMOTDPlatform;
import xyz.jpenilla.minimotd.common.UpdateChecker;
import xyz.jpenilla.minimotd.common.config.MiniMOTDConfig;
import xyz.jpenilla.minimotd.common.config.MiniMOTDConfig.PlayerCount;
import java.awt.image.BufferedImage;
import java.nio.file.Path;
@Plugin(
id = "{project.name}",
name = "{rootProject.name}",
version = "{version}",
description = "{description}",
url = "{url}",
id = "${project.name}",
name = Constants.PluginMetadata.NAME,
version = Constants.PluginMetadata.VERSION,
description = "${description}",
url = "${url}",
authors = {"jmp"}
)
public class MiniMOTDPlugin implements MiniMOTDPlatform<Favicon> {
public final class MiniMOTDPlugin implements MiniMOTDPlatform<Favicon> {
private final MiniMOTD<Favicon> miniMOTD;
private final ProxyServer server;
private final Logger logger;
private final MiniMessage miniMessage = MiniMessage.get();
private final PluginContainer pluginContainer;
private final CommandManager commandManager;
private final Path dataDirectory;
private final Metrics.Factory metricsFactory;
@@ -74,14 +75,12 @@ public class MiniMOTDPlugin implements MiniMOTDPlatform<Favicon> {
final @NonNull ProxyServer server,
final @NonNull Logger logger,
final @NonNull CommandManager commandManager,
final @NonNull PluginContainer pluginContainer,
@DataDirectory final @NonNull Path dataDirectory,
final Metrics.@NonNull Factory metricsFactory
) {
this.server = server;
this.logger = logger;
this.commandManager = commandManager;
this.pluginContainer = pluginContainer;
this.dataDirectory = dataDirectory;
this.metricsFactory = metricsFactory;
this.miniMOTD = new MiniMOTD<>(this);
@@ -90,31 +89,56 @@ public class MiniMOTDPlugin implements MiniMOTDPlatform<Favicon> {
@Subscribe
public void onProxyInitialization(final @NonNull ProxyInitializeEvent event) {
this.commandManager.register(this.commandManager.metaBuilder("minimotd").build(), new VelocityCommand(this));
this.registerCommand();
this.metricsFactory.make(this, 10257);
if (this.miniMOTD.configManager().pluginSettings().updateChecker()) {
this.server.getScheduler().buildTask(this, () ->
new UpdateChecker(this.pluginDescription().getVersion().orElse("")).checkVersion().forEach(this.logger::info)
this.server.getScheduler().buildTask(
this,
() -> new UpdateChecker().checkVersion().forEach(this.logger::info)
).schedule();
}
}
private void registerCommand() {
final class WrappingExecutor implements Command<CommandSource> {
private final CommandHandlerFactory.CommandHandler handler;
WrappingExecutor(final CommandHandlerFactory.@NonNull CommandHandler handler) {
this.handler = handler;
}
@Override
public int run(final @NonNull CommandContext<CommandSource> context) {
this.handler.execute(context.getSource());
return Command.SINGLE_SUCCESS;
}
}
final CommandHandlerFactory handlerFactory = new CommandHandlerFactory(this.miniMOTD);
this.commandManager.register(this.commandManager.metaBuilder("minimotd").build(), new BrigadierCommand(
LiteralArgumentBuilder.<CommandSource>literal("minimotd")
.requires(source -> source.hasPermission("minimotd.admin"))
.then(LiteralArgumentBuilder.<CommandSource>literal("help").executes(new WrappingExecutor(handlerFactory.help())))
.then(LiteralArgumentBuilder.<CommandSource>literal("about").executes(new WrappingExecutor(handlerFactory.about())))
.then(LiteralArgumentBuilder.<CommandSource>literal("reload").executes(new WrappingExecutor(handlerFactory.reload())))
));
}
@Subscribe
public void onServerListPing(final @NonNull ProxyPingEvent ping) {
public void onServerListPing(final @NonNull ProxyPingEvent event) {
final String configString = this.miniMOTD.configManager().pluginSettings().configStringForHost(
ping.getConnection().getVirtualHost()
event.getConnection().getVirtualHost()
.map(inetSocketAddress -> String.format("%s:%s", inetSocketAddress.getHostName(), inetSocketAddress.getPort()))
.orElse("default")
).orElse("default");
final MiniMOTDConfig config = this.miniMOTD.configManager().resolveConfig(configString);
final ServerPing.Builder pong = ping.getPing().asBuilder();
final ServerPing.Builder pong = event.getPing().asBuilder();
final int onlinePlayers = this.miniMOTD.calculateOnlinePlayers(config, pong.getOnlinePlayers());
final PlayerCount count = config.modifyPlayerCount(pong.getOnlinePlayers(), pong.getMaximumPlayers());
final int onlinePlayers = count.onlinePlayers();
final int maxPlayers = count.maxPlayers();
pong.onlinePlayers(onlinePlayers);
final int maxPlayers = config.adjustedMaxPlayers(onlinePlayers, pong.getMaximumPlayers());
pong.maximumPlayers(maxPlayers);
final MOTDIconPair<Favicon> pair = this.miniMOTD.createMOTD(config, onlinePlayers, maxPlayers);
@@ -123,24 +147,19 @@ public class MiniMOTDPlugin implements MiniMOTDPlatform<Favicon> {
pong.favicon(favicon);
}
final String motdString = pair.motd();
if (motdString != null) {
Component motdComponent = this.miniMessage.parse(motdString);
if (pong.getVersion().getProtocol() < Constants.MINECRAFT_1_16_PROTOCOL_VERSION) {
motdComponent = GsonComponentSerializer.colorDownsamplingGson().deserialize(GsonComponentSerializer.colorDownsamplingGson().serialize(motdComponent));
}
final Component motdComponent = pair.motd();
if (motdComponent != null) {
pong.description(motdComponent);
}
if (config.disablePlayerListHover()) {
pong.clearSamplePlayers();
}
if (config.hidePlayerCount()) {
pong.nullPlayers();
}
ping.setPing(pong.build());
}
public @NonNull MiniMOTD<Favicon> miniMOTD() {
return this.miniMOTD;
event.setPing(pong.build());
}
@Override
@@ -158,11 +177,8 @@ public class MiniMOTDPlugin implements MiniMOTDPlatform<Favicon> {
return Favicon.create(image);
}
public @NonNull MiniMessage miniMessage() {
return this.miniMessage;
}
public @NonNull PluginDescription pluginDescription() {
return this.pluginContainer.getDescription();
@Override
public void onReload() {
this.miniMOTD.configManager().loadExtraConfigs();
}
}
@@ -1,109 +0,0 @@
/*
* This file is part of MiniMOTD, licensed under the MIT License.
*
* Copyright (c) 2021 Jason Penilla
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package xyz.jpenilla.minimotd.velocity;
import com.google.common.collect.ImmutableList;
import com.velocitypowered.api.command.SimpleCommand;
import net.kyori.adventure.identity.Identity;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.List;
public class VelocityCommand implements SimpleCommand {
private final MiniMOTDPlugin plugin;
public VelocityCommand(final @NonNull MiniMOTDPlugin plugin) {
this.plugin = plugin;
}
@Override
public void execute(final @NonNull Invocation invocation) {
final String[] args = invocation.arguments();
if (args.length == 0) {
this.onInvalidUse(invocation);
return;
}
switch (args[0]) {
case "about":
this.onAbout(invocation);
return;
case "help":
this.onHelp(invocation);
return;
case "reload":
this.onReload(invocation);
return;
}
this.onInvalidUse(invocation);
}
private void onInvalidUse(final @NonNull Invocation invocation) {
this.sendMessages(invocation, ImmutableList.of("<hover:show_text:'<green>Click for <yellow>/minimotd help'><click:run_command:/minimotd help><italic><gradient:red:gold>Invalid usage.</gradient> <blue>Try <yellow>/minimotd help</yellow> or click here"));
}
private void onReload(final @NonNull Invocation invocation) {
this.plugin.miniMOTD().iconManager().loadIcons();
this.plugin.miniMOTD().configManager().loadExtraConfigs();
this.plugin.miniMOTD().configManager().loadConfigs();
this.sendMessages(invocation, ImmutableList.of("<green>Done reloading."));
}
private void onHelp(final @NonNull Invocation invocation) {
this.sendMessages(invocation, ImmutableList.of(
"<gradient:blue:green>MiniMOTD Commands:",
" <gray>-</gray> <hover:show_text:'<green>Click for <yellow>/minimotd about'><click:run_command:/minimotd about><yellow>/minimotd about",
" <gray>-</gray> <hover:show_text:'<green>Click for <yellow>/minimotd reload'><click:run_command:/minimotd reload><yellow>/minimotd reload",
" <gray>-</gray> <hover:show_text:'<green>Click for <yellow>/minimotd help'><click:run_command:/minimotd help><yellow>/minimotd help"
));
}
private void onAbout(final @NonNull Invocation invocation) {
final String header = "<gradient:white:black>=============</gradient><gradient:black:white>=============";
this.sendMessages(invocation, ImmutableList.of(
header,
"<hover:show_text:'<rainbow>click me!'><click:open_url:" + this.plugin.pluginDescription().getUrl().orElse("") + ">" + this.plugin.pluginDescription().getName().orElse("") + " <gradient:red:yellow>" + this.plugin.pluginDescription().getVersion().orElse(""),
"<yellow>By</yellow><gray>:</gray> <gradient:blue:green>jmp",
header
));
}
private void sendMessages(final @NonNull Invocation invocation, final @NonNull List<String> messages) {
messages.forEach(message -> invocation.source().sendMessage(Identity.nil(), this.plugin.miniMessage().parse(message)));
}
private static final List<String> COMMANDS = ImmutableList.of("about", "reload", "help");
@Override
public List<String> suggest(final @NonNull Invocation invocation) {
return COMMANDS;
}
@Override
public boolean hasPermission(final @NonNull Invocation invocation) {
return invocation.source().hasPermission("minimotd.admin");
}
}