splittet project to core and common functionalities

This commit is contained in:
2025-04-04 20:08:53 +02:00
parent 71d5d8303d
commit 6d0913fa0c
203 changed files with 780 additions and 726 deletions

View File

@ -0,0 +1,118 @@
package eu.mhsl.craftattack.core;
import eu.mhsl.craftattack.core.api.client.RepositoryLoader;
import eu.mhsl.craftattack.core.api.server.HttpServer;
import eu.mhsl.craftattack.core.appliance.Appliance;
import eu.mhsl.craftattack.core.config.Configuration;
import org.bukkit.Bukkit;
import org.bukkit.event.HandlerList;
import org.bukkit.plugin.java.JavaPlugin;
import org.reflections.Reflections;
import java.lang.reflect.ParameterizedType;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
public final class Main extends JavaPlugin {
private static Main instance;
private static Logger logger;
private List<Appliance> appliances;
private RepositoryLoader repositoryLoader;
@Override
public void onEnable() {
instance = this;
logger = instance().getLogger();
this.saveDefaultConfig();
try {
this.wrappedEnable();
} catch(Exception e) {
Main.logger().log(Level.SEVERE, "Error while initializing Spawn plugin, shutting down!", e);
Bukkit.shutdown();
}
}
private void wrappedEnable() {
Configuration.readConfig();
List<String> disabledAppliances = Configuration.pluginConfig.getStringList("disabledAppliances");
Main.logger().info("Loading Repositories...");
this.repositoryLoader = new RepositoryLoader();
Main.logger().info(String.format("Loaded %d repositories!", this.repositoryLoader.getRepositories().size()));
Main.logger().info("Loading appliances...");
Reflections reflections = new Reflections("eu.mhsl.craftattack.spawn");
Set<Class<? extends Appliance>> applianceClasses = reflections.getSubTypesOf(Appliance.class);
this.appliances = applianceClasses.stream()
.filter(applianceClass -> !disabledAppliances.contains(applianceClass.getSimpleName()))
.map(applianceClass -> {
try {
return (Appliance) applianceClass.getDeclaredConstructor().newInstance();
} catch(Exception e) {
throw new RuntimeException(String.format("Failed to create instance of '%s'", applianceClass.getName()), e);
}
})
.toList();
Main.logger().info(String.format("Loaded %d appliances!", this.appliances.size()));
Main.logger().info("Initializing appliances...");
this.appliances.forEach(appliance -> {
appliance.onEnable();
appliance.initialize(this);
});
Main.logger().info(String.format("Initialized %d appliances!", this.appliances.size()));
Main.logger().info("Starting HTTP API...");
new HttpServer();
this.getServer().getMessenger().registerOutgoingPluginChannel(this, "BungeeCord");
Main.logger().info("Startup complete!");
}
@Override
public void onDisable() {
Main.logger().info("Disabling appliances...");
this.appliances.forEach(appliance -> {
Main.logger().info("Disabling " + appliance.getClass().getSimpleName());
appliance.onDisable();
appliance.destruct(this);
});
HandlerList.unregisterAll(this);
Bukkit.getScheduler().cancelTasks(this);
Main.logger().info("Disabled " + this.appliances.size() + " appliances!");
}
public <T extends Appliance> T getAppliance(Class<T> clazz) {
return this.appliances.stream()
.filter(clazz::isInstance)
.map(clazz::cast)
.findFirst()
.orElseThrow(() -> new RuntimeException(String.format("Appliance %s not loaded or instantiated!", clazz)));
}
@SuppressWarnings("unchecked")
public static <T> Class<T> getApplianceType(Class<?> clazz) {
return (Class<T>) ((ParameterizedType) clazz.getGenericSuperclass()).getActualTypeArguments()[0];
}
public List<Appliance> getAppliances() {
return this.appliances;
}
public RepositoryLoader getRepositoryLoader() {
return this.repositoryLoader;
}
public static Main instance() {
return instance;
}
public static Logger logger() {
return logger;
}
}

View File

@ -0,0 +1,91 @@
package eu.mhsl.craftattack.core.api.client;
import eu.mhsl.craftattack.core.Main;
import org.apache.http.client.utils.URIBuilder;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.function.Consumer;
@RepositoryLoader.IgnoreRepository
public abstract class HttpRepository extends Repository {
private final Consumer<URIBuilder> baseUriBuilder;
public HttpRepository(URI basePath) {
this(basePath, null);
}
public HttpRepository(URI basePath, @Nullable Consumer<URIBuilder> baseUriBuilder) {
super(basePath);
this.baseUriBuilder = baseUriBuilder == null
? uriBuilder -> {
}
: baseUriBuilder;
}
protected <TInput, TOutput> ReqResp<TOutput> post(String command, TInput data, Class<TOutput> outputType) {
return this.post(command, parameters -> {
}, data, outputType);
}
protected <TInput, TOutput> ReqResp<TOutput> post(String command, Consumer<URIBuilder> parameters, TInput data, Class<TOutput> outputType) {
HttpRequest request = this.getRequestBuilder(this.getUri(command, parameters))
.POST(HttpRequest.BodyPublishers.ofString(this.gson.toJson(data)))
.build();
return this.execute(request, outputType);
}
protected <TOutput> ReqResp<TOutput> get(String command, Class<TOutput> outputType) {
return this.get(command, parameters -> {
}, outputType);
}
protected <TOutput> ReqResp<TOutput> get(String command, Consumer<URIBuilder> parameters, Class<TOutput> outputType) {
HttpRequest request = this.getRequestBuilder(this.getUri(command, parameters))
.GET()
.build();
return this.execute(request, outputType);
}
private URI getUri(String command, Consumer<URIBuilder> parameters) {
try {
URIBuilder builder = new URIBuilder(this.basePath + "/" + command);
this.baseUriBuilder.accept(builder);
parameters.accept(builder);
return builder.build();
} catch(URISyntaxException e) {
throw new RuntimeException(e);
}
}
private HttpRequest.Builder getRequestBuilder(URI endpoint) {
return HttpRequest.newBuilder()
.uri(endpoint)
.header("User-Agent", Main.instance().getServer().getBukkitVersion())
.header("Content-Type", "application/json");
}
private <TResponse> ReqResp<TResponse> execute(HttpRequest request, Class<TResponse> clazz) {
ReqResp<String> rawResponse = this.sendHttp(request);
return new ReqResp<>(rawResponse.status(), this.gson.fromJson(rawResponse.data(), clazz));
}
private ReqResp<String> sendHttp(HttpRequest request) {
try(HttpClient client = HttpClient.newHttpClient()) {
this.validateThread(request.uri().getPath());
HttpResponse<String> httpResponse = client.send(request, HttpResponse.BodyHandlers.ofString());
return new ReqResp<>(httpResponse.statusCode(), httpResponse.body());
} catch(IOException | InterruptedException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -0,0 +1,27 @@
package eu.mhsl.craftattack.core.api.client;
import com.google.gson.Gson;
import eu.mhsl.craftattack.core.Main;
import org.bukkit.Bukkit;
import java.net.URI;
public abstract class Repository {
protected URI basePath;
protected Gson gson;
public Repository(URI basePath) {
this.basePath = basePath;
this.gson = new Gson();
}
protected void validateThread(String commandName) {
if(!Bukkit.isPrimaryThread()) return;
Main.logger().warning(String.format(
"Repository '%s' was called synchronously with command '%s'!",
this.getClass().getSimpleName(),
commandName
));
}
}

View File

@ -0,0 +1,48 @@
package eu.mhsl.craftattack.core.api.client;
import org.apache.commons.lang3.NotImplementedException;
import org.reflections.Reflections;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.Set;
public class RepositoryLoader {
private final List<Repository> repositories;
@Retention(RetentionPolicy.RUNTIME)
public @interface IgnoreRepository {
}
public RepositoryLoader() {
Reflections reflections = new Reflections(this.getClass().getPackageName());
Set<Class<? extends Repository>> repositories = reflections.getSubTypesOf(Repository.class);
this.repositories = repositories.stream()
.filter(repository -> !repository.isAnnotationPresent(IgnoreRepository.class))
.map(repository -> {
try {
return (Repository) repository.getDeclaredConstructor().newInstance();
} catch(InstantiationException | IllegalAccessException | InvocationTargetException |
NoSuchMethodException e) {
throw new RuntimeException(e);
}
})
.toList();
}
public <T> T getRepository(Class<T> clazz) {
//noinspection unchecked
return this.repositories.stream()
.filter(clazz::isInstance)
.map(repository -> (T) repository)
.findFirst()
.orElseThrow(() -> new NotImplementedException(String.format("Repository '%s' not found!", clazz.getSimpleName())));
}
public List<Repository> getRepositories() {
return this.repositories;
}
}

View File

@ -0,0 +1,4 @@
package eu.mhsl.craftattack.core.api.client;
public record ReqResp<TData>(int status, TData data) {
}

View File

@ -0,0 +1,29 @@
package eu.mhsl.craftattack.core.api.client.repositories;
import eu.mhsl.craftattack.core.api.client.HttpRepository;
import eu.mhsl.craftattack.core.api.client.ReqResp;
import eu.mhsl.craftattack.core.util.api.EventApiUtil;
import java.util.UUID;
public class EventRepository extends HttpRepository {
public EventRepository() {
super(EventApiUtil.getBaseUri());
}
public record CreatedRoom(UUID uuid) {
}
public record QueueRoom(UUID player, UUID room) {
public record Response(String error) {
}
}
public ReqResp<CreatedRoom> createSession() {
return this.post("room", null, CreatedRoom.class);
}
public ReqResp<QueueRoom.Response> queueRoom(QueueRoom request) {
return this.post("queueRoom", request, QueueRoom.Response.class);
}
}

View File

@ -0,0 +1,27 @@
package eu.mhsl.craftattack.core.api.client.repositories;
import com.google.common.reflect.TypeToken;
import eu.mhsl.craftattack.core.api.client.HttpRepository;
import eu.mhsl.craftattack.core.api.client.ReqResp;
import eu.mhsl.craftattack.core.util.api.WebsiteApiUtil;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;
import java.util.UUID;
public class FeedbackRepository extends HttpRepository {
public FeedbackRepository() {
super(WebsiteApiUtil.getBaseUri(), WebsiteApiUtil::withAuthorizationSecret);
}
public record Request(String event, List<UUID> users) {
}
public ReqResp<Map<UUID, String>> createFeedbackUrls(Request data) {
final Type responseType = new TypeToken<Map<UUID, String>>() {
}.getType();
ReqResp<Object> rawData = this.post("feedback", data, Object.class);
return new ReqResp<>(rawData.status(), this.gson.fromJson(this.gson.toJson(rawData.data()), responseType));
}
}

View File

@ -0,0 +1,57 @@
package eu.mhsl.craftattack.core.api.client.repositories;
import eu.mhsl.craftattack.core.api.client.HttpRepository;
import eu.mhsl.craftattack.core.api.client.ReqResp;
import eu.mhsl.craftattack.core.util.api.WebsiteApiUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.UUID;
public class ReportRepository extends HttpRepository {
public ReportRepository() {
super(WebsiteApiUtil.getBaseUri(), WebsiteApiUtil::withAuthorizationSecret);
}
public record ReportCreationInfo(@NotNull UUID reporter, @Nullable UUID reported, String reason) {
}
public record ReportUrl(@NotNull String url) {
}
public record PlayerReports(
List<Report> from_self,
Object to_self
) {
public record Report(
@Nullable Reporter reported,
@NotNull String subject,
boolean draft,
@NotNull String status,
@NotNull String url
) {
public record Reporter(
@NotNull String username,
@NotNull String uuid
) {
}
}
}
public ReqResp<PlayerReports> queryReports(UUID player) {
return this.get(
"report",
(parameters) -> parameters.addParameter("uuid", player.toString()),
PlayerReports.class
);
}
public ReqResp<ReportUrl> createReport(ReportCreationInfo data) {
return this.post(
"report",
data,
ReportUrl.class
);
}
}

View File

@ -0,0 +1,31 @@
package eu.mhsl.craftattack.core.api.client.repositories;
import eu.mhsl.craftattack.core.api.client.HttpRepository;
import eu.mhsl.craftattack.core.api.client.ReqResp;
import eu.mhsl.craftattack.core.util.api.WebsiteApiUtil;
import java.util.UUID;
public class WhitelistRepository extends HttpRepository {
public WhitelistRepository() {
super(WebsiteApiUtil.getBaseUri(), WebsiteApiUtil::withAuthorizationSecret);
}
public record UserData(
UUID uuid,
String username,
String firstname,
String lastname,
Long banned_until,
Long outlawed_until
) {
}
public ReqResp<UserData> getUserData(UUID userId) {
return this.get(
"user",
parameters -> parameters.addParameter("uuid", userId.toString()),
UserData.class
);
}
}

View File

@ -0,0 +1,76 @@
package eu.mhsl.craftattack.core.api.server;
import com.google.gson.Gson;
import eu.mhsl.craftattack.core.Main;
import eu.mhsl.craftattack.core.appliance.Appliance;
import org.bukkit.configuration.ConfigurationSection;
import spark.Request;
import spark.Spark;
import java.util.function.Function;
import java.util.function.Supplier;
public class HttpServer {
private final ConfigurationSection apiConf = Main.instance().getConfig().getConfigurationSection("api");
protected final Gson gson = new Gson();
public static Object nothing = null;
public HttpServer() {
Spark.port(8080);
Spark.get("/ping", (request, response) -> System.currentTimeMillis());
Main.instance().getAppliances().forEach(appliance -> appliance.httpApi(new ApiBuilder(appliance)));
}
public record Response(Status status, Object error, Object response) {
public enum Status {
FAILURE,
SUCCESS
}
}
public class ApiBuilder {
@FunctionalInterface
public interface RequestProvider<TParsed, TOriginal, TResponse> {
TResponse apply(TParsed parsed, TOriginal original);
}
private final String applianceName;
private ApiBuilder(Appliance appliance) {
this.applianceName = appliance.getClass().getSimpleName().toLowerCase();
}
public void get(String path, Function<Request, Object> onCall) {
Spark.get(this.buildRoute(path), (req, resp) -> this.process(() -> onCall.apply(req)));
}
public void rawPost(String path, Function<Request, Object> onCall) {
Spark.post(this.buildRoute(path), (req, resp) -> this.process(() -> onCall.apply(req)));
}
public <TRequest> void post(String path, Class<TRequest> clazz, RequestProvider<TRequest, Request, Object> onCall) {
Spark.post(this.buildRoute(path), (req, resp) -> {
Main.instance().getLogger().info(req.body());
TRequest parsed = HttpServer.this.gson.fromJson(req.body(), clazz);
return this.process(() -> onCall.apply(parsed, req));
});
}
public String buildRoute(String path) {
return String.format("/api/%s/%s", this.applianceName, path);
}
private String process(Supplier<Object> exec) {
HttpServer.Response response;
try {
response = new Response(Response.Status.SUCCESS, null, exec.get());
} catch(Exception e) {
response = new Response(Response.Status.FAILURE, e, null);
}
return HttpServer.this.gson.toJson(response);
}
}
}

View File

@ -0,0 +1,124 @@
package eu.mhsl.craftattack.core.appliance;
import eu.mhsl.craftattack.core.Main;
import eu.mhsl.craftattack.core.api.client.Repository;
import eu.mhsl.craftattack.core.api.server.HttpServer;
import eu.mhsl.craftattack.core.config.Configuration;
import org.bukkit.Bukkit;
import org.bukkit.command.PluginCommand;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
/**
* Any implementation of this class can be seen as a "sub-plugin" with its own event handlers and commands.
* Appliances can be enabled or disabled independent of other appliances
*/
public abstract class Appliance {
private String localConfigPath;
private List<Listener> listeners;
private List<ApplianceCommand<?>> commands;
public Appliance() {
}
/**
* Use this constructor to specify a config sub-path for use with the localConfig() method.
*
* @param localConfigPath sub path, if not found, the whole config will be used
*/
public Appliance(String localConfigPath) {
this.localConfigPath = localConfigPath;
}
/**
* Provides a list of listeners for the appliance. All listeners will be automatically registered.
*
* @return List of listeners
*/
@NotNull
protected List<Listener> listeners() {
return new ArrayList<>();
}
/**
* Provides a list of commands for the appliance. All commands will be automatically registered.
*
* @return List of commands
*/
@NotNull
protected List<ApplianceCommand<?>> commands() {
return new ArrayList<>();
}
/**
* Called on initialization to add all needed API Routes.
* The routeBuilder can be used to get the correct Path prefixes
*
* @param apiBuilder holds data for needed route prefixes.
*/
public void httpApi(HttpServer.ApiBuilder apiBuilder) {
}
/**
* Provides a localized config section. Path can be set in appliance constructor.
*
* @return Section of configuration for your appliance
*/
@NotNull
public ConfigurationSection localConfig() {
return Optional.ofNullable(Configuration.cfg.getConfigurationSection(this.localConfigPath))
.orElseGet(() -> Configuration.cfg.createSection(this.localConfigPath));
}
public void onEnable() {
}
public void onDisable() {
}
public void initialize(@NotNull JavaPlugin plugin) {
this.listeners = this.listeners();
this.commands = this.commands();
this.listeners.forEach(listener -> Bukkit.getPluginManager().registerEvents(listener, plugin));
this.commands.forEach(command -> this.setCommandExecutor(plugin, command.commandName, command));
}
public void destruct(@NotNull JavaPlugin plugin) {
this.listeners.forEach(HandlerList::unregisterAll);
}
protected <T extends Appliance> T queryAppliance(Class<T> clazz) {
return Main.instance().getAppliance(clazz);
}
protected <T extends Repository> T queryRepository(Class<T> clazz) {
return Main.instance().getRepositoryLoader().getRepository(clazz);
}
private void setCommandExecutor(JavaPlugin plugin, String name, ApplianceCommand<?> executor) {
PluginCommand command = plugin.getCommand(name);
if(command != null && executor != null) {
command.setExecutor(executor);
command.setTabCompleter(executor);
} else {
Main.logger().warning("Command " + name + " is not specified in plugin.yml!");
throw new RuntimeException("All commands must be registered in plugin.yml. Missing command: " + name);
}
}
public List<Listener> getListeners() {
return this.listeners;
}
public List<ApplianceCommand<?>> getCommands() {
return this.commands;
}
}

View File

@ -0,0 +1,100 @@
package eu.mhsl.craftattack.core.appliance;
import eu.mhsl.craftattack.core.Main;
import net.kyori.adventure.text.Component;
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.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Optional;
/**
* Utility class which enables command name definition over a constructor.
*/
public abstract class ApplianceCommand<T extends Appliance> extends CachedApplianceSupplier<T> implements TabCompleter, CommandExecutor {
public String commandName;
protected Component errorMessage = Component.text("Fehler: ").color(NamedTextColor.RED);
public ApplianceCommand(String command) {
this.commandName = command;
}
public ApplianceCommand(String command, Component errorMessage) {
this(command);
this.errorMessage = errorMessage;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
try {
this.execute(sender, command, label, args);
} catch(Error e) {
sender.sendMessage(this.errorMessage.append(Component.text(e.getMessage())));
} catch(Exception e) {
sender.sendMessage(this.errorMessage.append(Component.text("Interner Fehler")));
Main.logger().warning("Error executing appliance command " + this.commandName + ": " + e.getMessage());
e.printStackTrace(System.err);
return false;
}
return true;
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
return null;
}
protected List<String> tabCompleteReducer(List<String> response, String[] args) {
return response.stream().filter(s -> s.startsWith(args[args.length - 1])).toList();
}
protected abstract void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception;
/**
* Utility class for command which can only be used as a Player. You can access the executing player with the getPlayer() method.
*/
public static abstract class PlayerChecked<T extends Appliance> extends ApplianceCommand<T> {
private Player player;
private Component notPlayerMessage = Component.text("Dieser Command kann nur von Spielern ausgeführt werden!").color(NamedTextColor.RED);
public PlayerChecked(String command) {
super(command);
}
public PlayerChecked(String command, Component errorMessage) {
super(command, errorMessage);
}
public PlayerChecked(String command, @Nullable Component errorMessage, Component notPlayerMessage) {
super(command);
this.errorMessage = Optional.ofNullable(errorMessage).orElse(this.errorMessage);
this.notPlayerMessage = notPlayerMessage;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if(!(sender instanceof Player)) {
sender.sendMessage(this.notPlayerMessage);
return false;
}
this.player = (Player) sender;
return super.onCommand(sender, command, label, args);
}
public Player getPlayer() {
return this.player;
}
}
public static class Error extends RuntimeException {
public Error(String message) {
super(message);
}
}
}

View File

@ -0,0 +1,13 @@
package eu.mhsl.craftattack.core.appliance;
import org.bukkit.event.Listener;
/**
* Utility class which provides a specific, type save appliance.
* You can access the appliance with the protected 'appliance' field.
*
* @param <T> the type of your appliance
*/
public abstract class ApplianceListener<T extends Appliance> extends CachedApplianceSupplier<T> implements Listener {
}

View File

@ -0,0 +1,16 @@
package eu.mhsl.craftattack.core.appliance;
import eu.mhsl.craftattack.core.Main;
public class CachedApplianceSupplier<T extends Appliance> implements IApplianceSupplier<T> {
private final T appliance;
public CachedApplianceSupplier() {
this.appliance = Main.instance().getAppliance(Main.getApplianceType(this.getClass()));
}
@Override
public T getAppliance() {
return this.appliance;
}
}

View File

@ -0,0 +1,5 @@
package eu.mhsl.craftattack.core.appliance;
public interface IApplianceSupplier<T extends Appliance> {
T getAppliance();
}

View File

@ -0,0 +1,31 @@
package eu.mhsl.craftattack.core.config;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.configuration.ConfigurationSection;
import java.util.Optional;
public class ConfigUtil {
public static class Position {
public static Location paseLocation(ConfigurationSection section) {
return new Location(
Bukkit.getWorld(Optional.ofNullable(section.getString("world")).orElse("world")),
section.getDouble("x"),
section.getDouble("y"),
section.getDouble("z"),
(float) section.getDouble("yaw"),
(float) section.getDouble("pitch")
);
}
public static void writeLocation(ConfigurationSection section, Location location) {
section.set("world", location.getWorld().getName());
section.set("x", location.x());
section.set("y", location.y());
section.set("z", location.z());
section.set("yaw", location.getYaw());
section.set("pitch", location.getPitch());
}
}
}

View File

@ -0,0 +1,29 @@
package eu.mhsl.craftattack.core.config;
import eu.mhsl.craftattack.core.Main;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.File;
public class Configuration {
private static final String configName = "config.yml";
private static final File configFile = new File(Main.instance().getDataFolder().getAbsolutePath() + "/" + configName);
public static FileConfiguration cfg;
public static ConfigurationSection pluginConfig;
public static void readConfig() {
cfg = YamlConfiguration.loadConfiguration(configFile);
pluginConfig = cfg.getConfigurationSection("plugin");
}
public static void saveChanges() {
try {
cfg.save(configFile);
} catch(Exception e) {
Main.logger().warning("Could not save configuration: " + e.getMessage());
}
}
}

View File

@ -0,0 +1,54 @@
package eu.mhsl.craftattack.core.util;
import org.bukkit.Bukkit;
import org.bukkit.GameRule;
import org.bukkit.World;
import org.bukkit.entity.Player;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class IteratorUtil {
public static void worlds(Consumer<World> world) {
worlds(w -> w, world);
}
public static <T> void worlds(Function<World, T> selector, Consumer<T> selected) {
Bukkit.getWorlds().forEach(world -> selected.accept(selector.apply(world)));
}
public static void onlinePlayers(Consumer<Player> player) {
Bukkit.getOnlinePlayers().forEach(player);
}
public static void setGameRules(Map<GameRule<Boolean>, Boolean> rules, boolean inverse) {
rules.forEach((gameRule, value) -> IteratorUtil.worlds(world -> world.setGameRule(gameRule, value ^ inverse)));
}
public static void times(int times, Runnable callback) {
IntStream.range(0, times).forEach(value -> callback.run());
}
public static <T> void iterateListInGlobal(int sectionStart, List<T> list, BiConsumer<Integer, T> callback) {
IntStream.range(sectionStart, sectionStart + list.size())
.forEach(value -> callback.accept(value, list.get(value - sectionStart)));
}
public static void iterateLocalInGlobal(int sectionStart, int localLength, BiConsumer<Integer, Integer> callback) {
IntStream.range(sectionStart, sectionStart + localLength)
.forEach(value -> callback.accept(value, value + sectionStart));
}
public static <T> List<T> expandList(List<T> list, int targetSize, T defaultValue) {
return Stream.concat(
list.stream(),
Stream.generate(() -> defaultValue).limit(Math.max(0, targetSize - list.size()))
).collect(Collectors.toList());
}
}

View File

@ -0,0 +1,11 @@
package eu.mhsl.craftattack.core.util;
public class NumberUtil {
public static double map(double oldValue, double oldMin, double oldMax, double newMin, double newMax) {
double out = (((oldValue - oldMin) * (newMax - newMin)) / (oldMax - oldMin)) + newMin;
if(out > newMax) out = newMax;
if(out < newMin) out = newMin;
return out;
}
}

View File

@ -0,0 +1,23 @@
package eu.mhsl.craftattack.core.util.api;
import eu.mhsl.craftattack.core.config.Configuration;
import org.bukkit.configuration.ConfigurationSection;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Objects;
public class EventApiUtil {
private final static ConfigurationSection apiConfig = Objects.requireNonNull(Configuration.cfg.getConfigurationSection("event"));
public final static String basePath = apiConfig.getString("api");
public static URI getBaseUri() {
Objects.requireNonNull(basePath);
try {
return new URI(basePath);
} catch(URISyntaxException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -0,0 +1,14 @@
package eu.mhsl.craftattack.core.util.api;
public class HttpStatus {
public static final int OK = 200;
public static final int CREATED = 201;
public static final int ACCEPTED = 202;
public static final int NO_CONTENT = 204;
public static final int BAD_REQUEST = 400;
public static final int UNAUTHORIZED = 401;
public static final int FORBIDDEN = 403;
public static final int NOT_FOUND = 404;
public static final int INTERNAL_SERVER_ERROR = 500;
public static final int SERVICE_UNAVAILABLE = 503;
}

View File

@ -0,0 +1,28 @@
package eu.mhsl.craftattack.core.util.api;
import eu.mhsl.craftattack.core.config.Configuration;
import org.apache.http.client.utils.URIBuilder;
import org.bukkit.configuration.ConfigurationSection;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Objects;
public class WebsiteApiUtil {
private final static ConfigurationSection apiConfig = Objects.requireNonNull(Configuration.cfg.getConfigurationSection("api"));
public final static String basePath = apiConfig.getString("baseurl");
public final static String apiSecret = apiConfig.getString("secret");
public static URI getBaseUri() {
Objects.requireNonNull(basePath);
try {
return new URI(basePath);
} catch(URISyntaxException e) {
throw new RuntimeException(e);
}
}
public static void withAuthorizationSecret(URIBuilder builder) {
builder.addParameter("secret", apiSecret);
}
}

View File

@ -0,0 +1,89 @@
package eu.mhsl.craftattack.core.util.entity;
import eu.mhsl.craftattack.core.config.ConfigUtil;
import eu.mhsl.craftattack.core.config.Configuration;
import eu.mhsl.craftattack.core.util.world.ChunkUtils;
import org.bukkit.Location;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Villager;
import java.util.Objects;
import java.util.UUID;
import java.util.function.Consumer;
public class DisplayVillager {
private final Location location;
private Villager villager;
public DisplayVillager(UUID uuid, Location location, Consumer<Villager> villagerCreator) {
this.location = location;
try {
ChunkUtils.loadChunkAtLocation(this.location);
this.villager = (Villager) this.location.getWorld().getEntity(uuid);
Objects.requireNonNull(this.villager);
} catch(NullPointerException | IllegalArgumentException e) {
this.villager = this.getBaseVillager();
villagerCreator.accept(this.villager);
}
this.villager.teleport(this.location);
}
public Villager getVillager() {
return this.villager;
}
private Villager getBaseVillager() {
Villager villager = (Villager) this.location.getWorld().spawnEntity(this.location, EntityType.VILLAGER);
villager.setRemoveWhenFarAway(false);
villager.setInvulnerable(true);
villager.setPersistent(true);
villager.setGravity(false);
villager.setAI(false);
villager.setCollidable(false);
villager.setCustomNameVisible(true);
return villager;
}
public static class ConfigBound {
private final DisplayVillager villager;
private final ConfigurationSection config;
public ConfigBound(ConfigurationSection configurationSection, Consumer<Villager> villagerCreator) {
this.config = configurationSection;
Location location = ConfigUtil.Position.paseLocation(Objects.requireNonNull(this.config.getConfigurationSection("villagerLocation")));
this.villager = new DisplayVillager(
UUID.fromString(this.config.getString("uuid", UUID.randomUUID().toString())),
location,
villager -> {
this.config.set("uuid", villager.getUniqueId().toString());
Configuration.saveChanges();
villagerCreator.accept(villager);
}
);
}
public void updateLocation(Location location) {
ConfigUtil.Position.writeLocation(
Objects.requireNonNull(this.config.getConfigurationSection("villagerLocation")),
location
);
Configuration.saveChanges();
this.villager.getVillager().teleport(location);
}
public Villager getVillager() {
return this.villager.getVillager();
}
public UUID getUniqueId() {
return this.getVillager().getUniqueId();
}
}
}

View File

@ -0,0 +1,33 @@
package eu.mhsl.craftattack.core.util.entity;
import org.bukkit.Material;
import org.bukkit.Statistic;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
public class PlayerUtils {
public static void resetStatistics(Player player) {
for(Statistic statistic : Statistic.values()) {
for(Material material : Material.values()) {
try {
player.setStatistic(statistic, material, 0);
} catch(IllegalArgumentException e) {
break;
}
}
for(EntityType entityType : EntityType.values()) {
try {
player.setStatistic(statistic, entityType, 0);
} catch(IllegalArgumentException e) {
break;
}
}
try {
player.setStatistic(statistic, 0);
} catch(IllegalArgumentException ignored) {
}
}
}
}

View File

@ -0,0 +1,22 @@
package eu.mhsl.craftattack.core.util.inventory;
import com.destroystokyo.paper.profile.PlayerProfile;
import com.destroystokyo.paper.profile.ProfileProperty;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.SkullMeta;
import java.util.UUID;
public class HeadBuilder {
public static ItemStack getCustomTextureHead(String base64) {
ItemStack head = new ItemStack(Material.PLAYER_HEAD);
SkullMeta meta = (SkullMeta) head.getItemMeta();
PlayerProfile profile = Bukkit.createProfile(UUID.nameUUIDFromBytes(base64.getBytes()), null);
profile.setProperty(new ProfileProperty("textures", base64));
meta.setPlayerProfile(profile);
head.setItemMeta(meta);
return head;
}
}

View File

@ -0,0 +1,92 @@
package eu.mhsl.craftattack.core.util.inventory;
import eu.mhsl.craftattack.core.util.text.ComponentUtil;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;
public class ItemBuilder {
private final ItemStack itemStack;
@Contract(value = "_ -> new", pure = true)
public static @NotNull ItemBuilder of(Material material) {
return new ItemBuilder(material);
}
@Contract(value = "_ -> new", pure = true)
public static @NotNull ItemBuilder of(ItemStack itemStack) {
return new ItemBuilder(itemStack);
}
private ItemBuilder(Material material) {
this.itemStack = ItemStack.of(material);
}
private ItemBuilder(ItemStack itemStack) {
this.itemStack = itemStack;
}
public ItemBuilder displayName(Component displayName) {
return this.withMeta(itemMeta -> itemMeta.displayName(displayName));
}
public ItemBuilder displayName(Function<Component, Component> process) {
return this.displayName(process.apply(this.itemStack.displayName()));
}
public ItemBuilder lore(String text) {
return this.lore(text, 50, NamedTextColor.GRAY);
}
public ItemBuilder lore(String text, NamedTextColor color) {
return this.lore(text, 50, color);
}
public ItemBuilder lore(String text, int linebreak, NamedTextColor color) {
return this.withMeta(itemMeta -> itemMeta.lore(
ComponentUtil.lineBreak(text, linebreak)
.map(s -> Component.text(s, color))
.toList()
));
}
public ItemBuilder appendLore(Component text) {
List<Component> lore = this.itemStack.lore();
Objects.requireNonNull(lore, "Cannot append lore to Item without lore");
lore.add(text);
return this.withMeta(itemMeta -> itemMeta.lore(lore));
}
public ItemBuilder noStacking() {
return this.withMeta(itemMeta -> itemMeta.setMaxStackSize(1));
}
public ItemBuilder glint() {
return this.withMeta(itemMeta -> itemMeta.setEnchantmentGlintOverride(true));
}
public ItemBuilder amount(int amount) {
this.itemStack.setAmount(amount);
return this;
}
public ItemBuilder withMeta(Consumer<ItemMeta> callback) {
ItemMeta meta = this.itemStack.getItemMeta();
callback.accept(meta);
this.itemStack.setItemMeta(meta);
return this;
}
public ItemStack build() {
return this.itemStack;
}
}

View File

@ -0,0 +1,13 @@
package eu.mhsl.craftattack.core.util.inventory;
import net.kyori.adventure.text.Component;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
public class PlaceholderItems {
private static final Component emptyName = Component.text(" ");
public static final ItemStack grayStainedGlassPane = ItemBuilder.of(Material.GRAY_STAINED_GLASS_PANE)
.displayName(emptyName)
.noStacking()
.build();
}

View File

@ -0,0 +1,23 @@
package eu.mhsl.craftattack.core.util.listener;
import org.bukkit.entity.Entity;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryOpenEvent;
import java.util.UUID;
public class DismissInventoryOpenFromHolder implements Listener {
private final UUID inventoryHolder;
public DismissInventoryOpenFromHolder(UUID inventoryHolder) {
this.inventoryHolder = inventoryHolder;
}
@EventHandler
public void onInventoryOpen(InventoryOpenEvent event) {
if(event.getInventory().getHolder() instanceof Entity holder) {
if(holder.getUniqueId().equals(this.inventoryHolder)) event.setCancelled(true);
}
}
}

View File

@ -0,0 +1,24 @@
package eu.mhsl.craftattack.core.util.listener;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerInteractAtEntityEvent;
import java.util.UUID;
import java.util.function.Consumer;
public class PlayerInteractAtEntityEventListener implements Listener {
private final UUID interactableEntityUUID;
private final Consumer<PlayerInteractAtEntityEvent> callback;
public PlayerInteractAtEntityEventListener(UUID interactableEntityUUID, Consumer<PlayerInteractAtEntityEvent> callback) {
this.interactableEntityUUID = interactableEntityUUID;
this.callback = callback;
}
@EventHandler
public void onInteract(PlayerInteractAtEntityEvent event) {
if(!event.getRightClicked().getUniqueId().equals(this.interactableEntityUUID)) return;
this.callback.accept(event);
}
}

View File

@ -0,0 +1,46 @@
package eu.mhsl.craftattack.core.util.server;
import org.bukkit.entity.Player;
import org.geysermc.cumulus.form.SimpleForm;
import org.geysermc.floodgate.api.FloodgateApi;
import org.geysermc.floodgate.api.player.FloodgatePlayer;
import java.util.function.Consumer;
public class Floodgate {
private static final FloodgateApi instance = FloodgateApi.getInstance();
public static boolean isBedrock(Player p) {
return instance.isFloodgatePlayer(p.getUniqueId());
}
public static FloodgatePlayer getBedrockPlayer(Player p) {
return instance.getPlayer(p.getUniqueId());
}
public static void runBedrockOnly(Player p, Consumer<FloodgatePlayer> callback) {
if(isBedrock(p)) callback.accept(instance.getPlayer(p.getUniqueId()));
}
public static void runJavaOnly(Player p, Consumer<Player> callback) {
if(!isBedrock(p)) callback.accept(p);
}
public static void throwWithMessageWhenBedrock(Player player) {
if(isBedrock(player)) {
SimpleForm.builder()
.title("Nicht unterstützt")
.content("Bedrock-Spieler werden derzeit für diese Aktion unterstützt! Tut uns Leid.")
.button("Ok")
.build();
throw new BedrockNotSupportedException(player);
}
}
public static class BedrockNotSupportedException extends RuntimeException {
public BedrockNotSupportedException(Player player) {
super(String.format("Bedrock player '%s' tried using an Operation which is unsupported.", player.getName()));
}
}
}

View File

@ -0,0 +1,15 @@
package eu.mhsl.craftattack.core.util.server;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import eu.mhsl.craftattack.core.Main;
import org.bukkit.entity.Player;
public class PluginMessage {
public static void connect(Player player, String server) {
ByteArrayDataOutput output = ByteStreams.newDataOutput();
output.writeUTF("Connect");
output.writeUTF(server);
player.sendPluginMessage(Main.instance(), "BungeeCord", output.toByteArray());
}
}

View File

@ -0,0 +1,87 @@
package eu.mhsl.craftattack.core.util.statistics;
import eu.mhsl.craftattack.core.Main;
import net.kyori.adventure.util.Ticks;
import org.bukkit.Bukkit;
import org.bukkit.scheduler.BukkitTask;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.logging.Level;
public class NetworkMonitor {
private long previousRxBytes = 0;
private long previousTxBytes = 0;
private long previousRxPackets = 0;
private long previousTxPackets = 0;
private final String iFace;
private long rxBytesLastDuration = 0;
private long txBytesLastDuration = 0;
private long rxPacketsLastDuration = 0;
private long txPacketsLastDuration = 0;
private final BukkitTask updateTask;
public NetworkMonitor(String iFace, Duration sampleDuration) {
this.iFace = iFace;
this.updateTask = Bukkit.getScheduler().runTaskTimerAsynchronously(
Main.instance(),
this::update,
0,
sampleDuration.getSeconds() * Ticks.TICKS_PER_SECOND
);
}
public record Traffic(long rxBytes, long txBytes) {
}
public record Packets(long rxCount, long txCount) {
}
public Traffic getTraffic() {
return new Traffic(this.rxBytesLastDuration, this.txBytesLastDuration);
}
public Packets getPackets() {
return new Packets(this.rxPacketsLastDuration, this.txPacketsLastDuration);
}
public void stop() {
this.updateTask.cancel();
}
private void update() {
long rxBytes = this.getNetworkStatistic("rx_bytes");
long txBytes = this.getNetworkStatistic("tx_bytes");
long rxPackets = this.getNetworkStatistic("rx_packets");
long txPackets = this.getNetworkStatistic("tx_packets");
this.rxBytesLastDuration = rxBytes - this.previousRxBytes;
this.txBytesLastDuration = txBytes - this.previousTxBytes;
this.rxPacketsLastDuration = rxPackets - this.previousRxPackets;
this.txPacketsLastDuration = txPackets - this.previousTxPackets;
this.previousRxBytes = rxBytes;
this.previousTxBytes = txBytes;
this.previousRxPackets = rxPackets;
this.previousTxPackets = txPackets;
}
private long getNetworkStatistic(String statistic) {
try {
String path = String.format("/sys/class/net/%s/statistics/%s", this.iFace, statistic);
String content = new String(Files.readAllBytes(Paths.get(path)));
return Long.parseLong(content.trim());
} catch(IOException e) {
Main.logger().log(Level.SEVERE, "Statistics are only supported on Linux! Is tablist.interface config set correctly?");
this.stop();
throw new RuntimeException("Failed reading network statistic", e);
}
}
}

View File

@ -0,0 +1,12 @@
package eu.mhsl.craftattack.core.util.statistics;
import org.bukkit.Bukkit;
import java.util.Arrays;
public class ServerMonitor {
public static float getServerMSPT() {
long[] times = Bukkit.getServer().getTickTimes();
return ((float) Arrays.stream(times).sum() / times.length) * 1.0E-6f;
}
}

View File

@ -0,0 +1,27 @@
package eu.mhsl.craftattack.core.util.text;
import eu.mhsl.craftattack.core.util.NumberUtil;
import net.kyori.adventure.text.format.TextColor;
import java.awt.*;
public class ColorUtil {
public static TextColor mapGreenToRed(double value, double minValue, double maxValue, boolean lowIsGreen) {
float hue = (float) NumberUtil.map(value, minValue, maxValue, 0, 120);
if(lowIsGreen) {
hue = Math.abs(hue - 120);
}
return TextColor.color(Color.getHSBColor(hue / 360, 1f, 1f).getRGB());
}
public static TextColor msptColor(float mspt) {
if(mspt > 50) return TextColor.color(255, 0, 0);
return ColorUtil.mapGreenToRed(mspt, 25, 60, true);
}
public static TextColor tpsColor(float tps) {
return ColorUtil.mapGreenToRed(tps, 15, 20, false);
}
}

View File

@ -0,0 +1,189 @@
package eu.mhsl.craftattack.core.util.text;
import eu.mhsl.craftattack.core.util.statistics.NetworkMonitor;
import eu.mhsl.craftattack.core.util.statistics.ServerMonitor;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentBuilder;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.awt.*;
import java.lang.management.OperatingSystemMXBean;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class ComponentUtil {
public static TextComponent pleaseWait() {
return Component.text("Bitte warte einen Augenblick...", NamedTextColor.GRAY);
}
public static Component clearedSpace() {
return Component.text(" ").hoverEvent(Component.empty().asHoverEvent());
}
public static TextComponent appendWithNewline(Component a, Component b) {
return Component.text().append(a.appendNewline().append(b)).build();
}
public static TextComponent appendWithSpace(Component a, Component b) {
return Component.text().append(a).append(Component.text(" ")).append(b).build();
}
public static Stream<String> lineBreak(String text) {
return lineBreak(text, 50);
}
public static Stream<String> lineBreak(String text, int charactersPerLine) {
List<String> lines = new ArrayList<>();
String[] words = text.split(" ");
StringBuilder line = new StringBuilder();
for(String word : words) {
if(line.length() + word.length() + 1 > charactersPerLine) {
lines.add(line.toString().trim());
line = new StringBuilder();
}
line.append(word).append(" ");
}
if(!line.isEmpty()) {
lines.add(line.toString().trim());
}
return lines.stream();
}
public static String lineBreakNL(String text, int charactersPerLine) {
Stream<String> lines = lineBreak(text, charactersPerLine);
return lines.collect(Collectors.joining("\n"));
}
public static Component getFormattedTickTimes(boolean detailed) {
float mspt = ServerMonitor.getServerMSPT();
float roundedMspt = Math.round(mspt * 100f) / 100f;
int loadPercentage = (int) (Math.min(100, (mspt / 50.0) * 100));
float roundedTPS = Math.round(Bukkit.getTPS()[0] * 100f) / 100f;
TextColor percentageColor = ColorUtil.mapGreenToRed(loadPercentage, 80, 100, true);
ComponentBuilder<TextComponent, TextComponent.Builder> tickTimes = Component.text()
.append(Component.text("Serverlast: ", NamedTextColor.GRAY))
.append(Component.text(loadPercentage + "% ", percentageColor))
.appendNewline();
if(detailed) {
tickTimes
.append(Component.text(roundedMspt + "mspt", ColorUtil.msptColor(mspt)))
.append(Component.text(" | ", NamedTextColor.GRAY));
}
return tickTimes
.append(Component.text(roundedTPS + "tps", ColorUtil.tpsColor(roundedTPS)))
.build();
}
public static Component getFormattedPing(Player player) {
int playerPing = player.getPing();
int averagePing = Bukkit.getOnlinePlayers().stream()
.map(Player::getPing).reduce(Integer::sum)
.orElse(0) / Bukkit.getOnlinePlayers().size();
return Component.text()
.append(Component.text("Dein Ping: ", NamedTextColor.GRAY))
.append(Component.text(playerPing + "ms", ColorUtil.mapGreenToRed(playerPing, 50, 200, true)))
.append(Component.text(" | ", NamedTextColor.GRAY))
.append(Component.text("Durschnitt: ", NamedTextColor.GRAY))
.append(Component.text(averagePing + "ms", ColorUtil.mapGreenToRed(averagePing, 50, 200, true)))
.build();
}
public static Component createRainbowText(String text, int step) {
Component builder = Component.empty();
int hue = 0;
for(char c : text.toCharArray()) {
TextColor color = TextColor.color(Color.getHSBColor((float) hue / 360, 1, 1).getRGB());
builder = builder.append(Component.text(c).color(color));
hue += step;
}
return builder;
}
public static Component getFormattedNetworkStats(NetworkMonitor.Traffic traffic, NetworkMonitor.Packets packets) {
return Component.text()
.append(Component.text(
DataSizeConverter.convertBytesPerSecond(traffic.rxBytes()) + "" + NumberAbbreviation.abbreviateNumber(packets.rxCount()) + "pps",
NamedTextColor.GREEN
))
.append(Component.text(" | ", NamedTextColor.GRAY))
.append(Component.text(
DataSizeConverter.convertBytesPerSecond(traffic.txBytes()) + "" + NumberAbbreviation.abbreviateNumber(packets.rxCount()) + "pps",
NamedTextColor.RED
))
.build();
}
public static Component getFormattedSystemStats(OperatingSystemMXBean systemMonitor) {
if(!(systemMonitor instanceof com.sun.management.OperatingSystemMXBean monitor))
return Component.text("Could not get System information", NamedTextColor.DARK_GRAY);
return Component.text()
.append(Component.text("proc: ", NamedTextColor.GRAY))
.append(Component.text(
String.format("%.0f%%cpu", monitor.getProcessCpuLoad() * 100),
NamedTextColor.GOLD
))
.append(Component.text(" | ", NamedTextColor.GRAY))
.append(Component.text(
String.format("%s time", DataSizeConverter.formatCpuTimeToHumanReadable(monitor.getProcessCpuTime())),
NamedTextColor.LIGHT_PURPLE
))
.append(Component.text(" | ", NamedTextColor.GRAY))
.append(Component.text(
String.format(
"%s free, %s committed RAM",
DataSizeConverter.formatBytesToHumanReadable(monitor.getFreeMemorySize()),
DataSizeConverter.formatBytesToHumanReadable(monitor.getCommittedVirtualMemorySize())
),
NamedTextColor.DARK_AQUA
))
.appendNewline()
.append(Component.text("sys: ", NamedTextColor.GRAY))
.append(Component.text(
String.format("%.0f%%cpu", monitor.getCpuLoad() * 100),
NamedTextColor.GOLD
))
.append(Component.text(" | ", NamedTextColor.GRAY))
.append(Component.text(
String.format(
"1min %.2f load avg (%.0f%%)",
monitor.getSystemLoadAverage(),
(monitor.getSystemLoadAverage() / monitor.getAvailableProcessors()) * 100
),
NamedTextColor.LIGHT_PURPLE
))
.append(Component.text(" | ", NamedTextColor.GRAY))
.append(Component.text(
String.format("%s total RAM", DataSizeConverter.formatBytesToHumanReadable(monitor.getTotalMemorySize())),
NamedTextColor.DARK_AQUA
))
.appendNewline()
.append(Component.text(
String.format(
"%s(%s) \uD83D\uDE80 on %s with %s cpu(s)",
monitor.getName(),
monitor.getVersion(),
monitor.getArch(),
monitor.getAvailableProcessors()
),
NamedTextColor.GRAY
))
.build();
}
}

View File

@ -0,0 +1,101 @@
package eu.mhsl.craftattack.core.util.text;
import eu.mhsl.craftattack.core.Main;
import net.kyori.adventure.text.Component;
import org.bukkit.Bukkit;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
public class Countdown {
private final int countdownFrom;
private boolean running;
private int taskId;
private int current;
private final Consumer<Component> announcementConsumer;
private final Function<AnnouncementData, Component> announcementBuilder;
private Function<Integer, AnnouncementData> defaultAnnouncements;
private final List<CustomAnnouncements> customAnnouncements = new ArrayList<>();
private final Runnable onDone;
public record AnnouncementData(int count, String unit) {
}
public record CustomAnnouncements(Function<Integer, Boolean> test, Consumer<Integer> task) {
}
public Countdown(int countdownFrom, Function<AnnouncementData, Component> announcementBuilder, Consumer<Component> announcementConsumer, Runnable onDone) {
this.countdownFrom = countdownFrom;
this.current = countdownFrom;
this.announcementBuilder = announcementBuilder;
this.announcementConsumer = announcementConsumer;
this.onDone = onDone;
this.defaultAnnouncements = count -> {
if(this.current > 60 && this.current % 60 == 0) {
return new AnnouncementData(this.current / 60, "Minuten");
}
if(this.current <= 60 && (this.current <= 10 || this.current % 10 == 0)) {
return new AnnouncementData(this.current, "Sekunden");
}
return null;
};
}
public void addCustomAnnouncement(CustomAnnouncements announcement) {
this.customAnnouncements.add(announcement);
}
public void setDefaultAnnouncements(Function<Integer, AnnouncementData> defaultAnnouncement) {
this.defaultAnnouncements = defaultAnnouncement;
}
public void start() {
if(this.running) throw new IllegalStateException("Countdown already running!");
this.running = true;
this.taskId = Bukkit.getScheduler().scheduleSyncRepeatingTask(Main.instance(), this::tick, 20, 20);
}
public void cancel() {
if(!this.running) throw new IllegalStateException("Countdown not running!");
this.running = false;
Bukkit.getScheduler().cancelTask(this.taskId);
this.taskId = 0;
this.current = this.countdownFrom;
}
public void cancelIfRunning() {
if(this.running) this.cancel();
}
private void tick() {
AnnouncementData defaultAnnouncementData = this.defaultAnnouncements.apply(this.current);
if(defaultAnnouncementData != null) {
this.announcementConsumer.accept(this.announcementBuilder.apply(defaultAnnouncementData));
}
this.customAnnouncements
.stream()
.filter(a -> a.test.apply(this.current))
.forEach(a -> a.task.accept(this.current));
this.current--;
if(this.isDone()) {
this.onDone.run();
this.cancel();
}
}
public boolean isDone() {
return this.current <= 0;
}
public boolean isRunning() {
return this.running;
}
}

View File

@ -0,0 +1,55 @@
package eu.mhsl.craftattack.core.util.text;
public class DataSizeConverter {
public static String convertBytesPerSecond(long bytes) {
double kbits = bytes * 8.0 / 1000.0;
double mbits = kbits / 1000.0;
if(mbits >= 1) {
return String.format("%.2f Mbit", mbits);
} else {
return String.format("%.2f Kbit", kbits);
}
}
public static String formatBytesToHumanReadable(long bytes) {
String[] units = {"B", "KB", "MB", "GB", "TB", "PB", "EB"};
int unitIndex = 0;
double readableSize = bytes;
while(readableSize >= 1024 && unitIndex < units.length - 1) {
readableSize /= 1024;
unitIndex++;
}
return String.format("%.2f%s", readableSize, units[unitIndex]);
}
public static String formatCpuTimeToHumanReadable(long nanoseconds) {
if(nanoseconds < 0) return "unsupported";
long seconds = nanoseconds / 1_000_000_000;
long minutes = seconds / 60;
long hours = minutes / 60;
long days = hours / 24;
seconds %= 60;
minutes %= 60;
hours %= 24;
return String.format("%dd%dh%dm%ds", days, hours, minutes, seconds);
}
public static String formatSecondsToHumanReadable(int seconds) {
if(seconds < 0) return "unsupported";
int minutes = seconds / 60;
int hours = minutes / 60;
int days = hours / 24;
seconds %= 60;
minutes %= 60;
hours %= 24;
return String.format("%dd %dh %dm %ds", days, hours, minutes, seconds);
}
}

View File

@ -0,0 +1,44 @@
package eu.mhsl.craftattack.core.util.text;
import eu.mhsl.craftattack.core.Main;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.util.UUID;
public record DisconnectInfo(String error, String description, String help, UUID user) {
public void applyKick(Player player) {
Bukkit.getScheduler().runTask(Main.instance(), () -> player.kick(this.getComponent()));
}
public Component getComponent() {
return Component.text()
.appendNewline().appendNewline()
.append(Component.text(this.error, NamedTextColor.DARK_RED)).appendNewline()
.append(Component.text(this.description, NamedTextColor.RED)).appendNewline().appendNewline()
.append(Component.text(this.help, NamedTextColor.GRAY)).appendNewline().appendNewline()
.append(Component.text(this.user.toString(), NamedTextColor.DARK_GRAY)).appendNewline()
.build();
}
public static class Throwable extends Exception {
public String error;
public String description;
public String help;
public UUID user;
public Throwable(String error, String description, String help, UUID user) {
super(description);
this.error = error;
this.description = description;
this.help = help;
this.user = user;
}
public DisconnectInfo getDisconnectScreen() {
return new DisconnectInfo(this.error, this.description, this.help, this.user);
}
}
}

View File

@ -0,0 +1,15 @@
package eu.mhsl.craftattack.core.util.text;
public class NumberAbbreviation {
public static <T extends Number & Comparable<T>> String abbreviateNumber(T number) {
double value = number.doubleValue();
if(value >= 1_000_000) {
return String.format("%.1fM", value / 1_000_000.0);
} else if(value >= 1_000) {
return String.format("%.1fk", value / 1_000.0);
} else {
return number.toString();
}
}
}

View File

@ -0,0 +1,34 @@
package eu.mhsl.craftattack.core.util.text;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.TextColor;
import java.awt.*;
public class RainbowComponent {
private float hueOffset = 0;
private final String text;
private final int density;
private final float speed;
public RainbowComponent(String text, int density, float speed) {
this.text = text;
this.density = density;
this.speed = speed;
}
public Component getRainbowState() {
Component builder = Component.empty();
float hue = this.hueOffset;
for(char c : this.text.toCharArray()) {
float normalizedHue = (hue % 360) / 360;
TextColor color = TextColor.color(Color.getHSBColor(normalizedHue, 1, 1).getRGB());
builder = builder.append(Component.text(c).color(color));
hue += this.density;
}
this.hueOffset = (this.hueOffset + this.speed) % 360;
return builder;
}
}

View File

@ -0,0 +1,29 @@
package eu.mhsl.craftattack.core.util.world;
import org.bukkit.Location;
import org.bukkit.Material;
import java.util.List;
public class BlockCycle {
private final Location location;
private final Material defaultBlock;
private final List<Material> blockList;
private int current = 0;
public BlockCycle(Location location, Material defaultBlock, List<Material> blockList) {
this.location = location;
this.defaultBlock = defaultBlock;
this.blockList = blockList;
}
public void next() {
this.location.getBlock().setType(this.blockList.get(this.current));
this.current++;
if(this.current >= this.blockList.size()) this.current = 0;
}
public void reset() {
this.location.getBlock().setType(this.defaultBlock);
}
}

View File

@ -0,0 +1,23 @@
package eu.mhsl.craftattack.core.util.world;
import org.bukkit.Chunk;
import org.bukkit.Location;
public class ChunkUtils {
public record ChunkPos(int x, int z) {
}
public static Chunk loadChunkAtLocation(Location location) {
ChunkPos chunkPos = locationToChunk(location);
return location.getWorld().getChunkAt(chunkPos.x, chunkPos.z);
}
public static ChunkPos locationToChunk(Location location) {
return new ChunkPos(floor(location.x()) >> 4, floor(location.z()) >> 4);
}
private static int floor(double num) {
int floor = (int) num;
return floor == num ? floor : floor - (int) (Double.doubleToRawLongBits(num) >>> 63);
}
}

View File

@ -0,0 +1,37 @@
package eu.mhsl.craftattack.core.util.world;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.sound.Sound;
import org.bukkit.entity.Player;
public class InteractSounds {
private final Player player;
public static InteractSounds of(Player player) {
return new InteractSounds(player);
}
private InteractSounds(Player player) {
this.player = player;
}
private void playSound(org.bukkit.Sound sound) {
this.player.playSound(this.getSound(sound.key()), Sound.Emitter.self());
}
private Sound getSound(Key soundKey) {
return Sound.sound(soundKey, Sound.Source.PLAYER, 1f, 1f);
}
public void click() {
this.playSound(org.bukkit.Sound.UI_BUTTON_CLICK);
}
public void success() {
this.playSound(org.bukkit.Sound.ENTITY_PLAYER_LEVELUP);
}
public void delete() {
this.playSound(org.bukkit.Sound.ENTITY_SILVERFISH_DEATH);
}
}

View File

@ -0,0 +1,73 @@
plugin:
disabledAppliances:
- NameOfApplianceClass
api:
secret: secret
baseurl: https://mhsl.eu/craftattack/api/
worldMuseum:
uuid:
connect-server-name: worldmuseum
villagerLocation:
world: world
x: 0
y: 0
z: 0
yaw: 0
pitch: 0
adminMarker:
permission: admin
color: AQUA
countdown:
enabled: false
start-permission: admin
countdown: 60
worldborder-before: 37
event:
api: http://10.20.6.5:8080/
connect-server-name: event
enabled: false
roomId:
uuid:
villagerLocation:
world: world
x: 0
y: 0
z: 0
yaw: 0
pitch: 0
help:
teamspeak: myserver.com
spawn: "Der Weltspawn befindet sich bei x:0 y:0 z:0"
playerLimit:
maxPlayers: 10
whitelist:
overrideIntegrityCheck: false
tablist:
interface: eth0
outlawed:
voluntarily: []
packselect:
packs:
- somepack:
name: "Texture pack name"
description: "Texture pack description"
author: "Pack Author(s)"
url: "https://example.com/download/pack.zip"
hash: "" # SHA1 hash of ZIP file (will be auto determined by the server on startup when not set)
icon: "" # base64 player-head texture, can be obtained from sites like https://minecraft-heads.com/ under developers > Value
endPrevent:
endDisabled: true
spawnpoint: