commit c10b295171c9e8340d2e1d567863dea8ecac0021 Author: Rainnny7 Date: Fri Feb 19 15:11:08 2021 -0500 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5104445 --- /dev/null +++ b/.gitignore @@ -0,0 +1,136 @@ +# Created by .ignore support plugin (hsz.mobi) +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Gradle template +.gradle +/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Cache of project +.gradletasknamecache + +# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +# gradle/wrapper/gradle-wrapper.properties + +# InsaneCore related +/output/ +/**/build/ +/.idea/ +.idea/codeStyles/ +.idea/compiler.xml +.idea/jarRepositories.xml +.idea/misc.xml +.idea/modules/ +.idea/vcs.xml +gradle/ +gradlew +gradlew.bat +!/gradle.properties +gradle.properties +### Gradle template +**/build/ +!src/**/build/ + +# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +# gradle/wrapper/gradle-wrapper.properties + +### Java template +# Compiled class file +*.class + +# Log file +*.log +*.log.lck + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..2322f71 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,19 @@ +image: gradle:jdk11 + +stages: + - build + - deploy + +build:prod: + stage: build + only: + - master + before_script: + - gradle wrapper + script: + - ./gradlew shadowJar + - ./gradlew publishMavenPublicationToApiRepository + artifacts: + paths: + - output + expire_in: 1 week diff --git a/README.md b/README.md new file mode 100644 index 0000000..aaa2aa5 --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# Project Structure +- **Commons**: Commons is common libraries and/or utilities that are shared across the network. +- **ServerData**: This branch of the project controls the database backend for both Redis and MySQL. All modules that use Redis or MySQL must have this as a dependency. +- **ServerController**: This will dynamically start and stop servers on demand. +- **Proxy**: The proxy will handle server balancing and player caching. +- **API**: This is the frontend of the project if you will. All developers will be given access to this branch of the project where they can access multiple parts of the server. +- **Core**: The core is a shared module between all Spigot plugins. Everything used between multiple Spigot servers will be created here. +- **Hub**: This is pretty self-explanatory. Any Hub related things will go here. +- **Testing**: This part of the project is strictly for testing purposes . + +# Redis +Redis is used for a variety of different things. Mainly, Redis is used for the server backend and as a global cache. + +# MySQL +MySQL is used for the main account backend. Everything related to accounts will be stored here. + +# Production +When something is being released for production, you must set the production parameter in the **MySQLController** class and make sure you are on the **master** branch! + +# Before Starting +- Create a **gradle.properties** file which will contain your +Nexus username and password. +- Stick to Java naming conventions, which can be found [here](https://www.oracle.com/java/technologies/javase/codeconventions-namingconventions.html) + +# Building From Source +- Clone the project +- Open the project in your IDE of choice (Intellij is highly recommended) +- Run the task **shadowJar** which can be found under **McGamerCore** » **Tasks** » **shadow** + +**If you are confused on how to do things, or you're unsure on where to put something, please contact Braydon on Discord** \ No newline at end of file diff --git a/api/build.gradle.kts b/api/build.gradle.kts new file mode 100644 index 0000000..e5735d0 --- /dev/null +++ b/api/build.gradle.kts @@ -0,0 +1,27 @@ +dependencies { + api(project(":serverdata")) + implementation("com.zaxxer:HikariCP:3.4.5") + implementation("mysql:mysql-connector-java:8.0.23") + compile("com.sparkjava:spark-core:2.9.3") + compile("com.google.guava:guava:30.1-jre") +} + +val jar by tasks.getting(Jar::class) { + manifest { + attributes["Main-Class"] = "zone.themcgamer.api.API" + } +} + +tasks { + processResources { + val tokens = mapOf("version" to project.version) + from(sourceSets["main"].resources.srcDirs) { + filter("tokens" to tokens) + } + } + + shadowJar { + archiveFileName.set("${project.rootProject.name}-${project.name}-v${project.version}.jar") + destinationDir = file("$rootDir/output") + } +} \ No newline at end of file diff --git a/api/src/main/java/zone/themcgamer/api/API.java b/api/src/main/java/zone/themcgamer/api/API.java new file mode 100644 index 0000000..1e3be45 --- /dev/null +++ b/api/src/main/java/zone/themcgamer/api/API.java @@ -0,0 +1,178 @@ +package zone.themcgamer.api; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import spark.Spark; +import zone.themcgamer.api.model.IModel; +import zone.themcgamer.api.model.ModelSerializer; +import zone.themcgamer.api.model.impl.*; +import zone.themcgamer.api.repository.AccountRepository; +import zone.themcgamer.api.route.AccountRoute; +import zone.themcgamer.api.route.ServersRoute; +import zone.themcgamer.api.route.StatusRoute; +import zone.themcgamer.data.APIAccessLevel; +import zone.themcgamer.data.jedis.JedisController; +import zone.themcgamer.data.jedis.data.APIKey; +import zone.themcgamer.data.jedis.repository.RedisRepository; +import zone.themcgamer.data.jedis.repository.impl.APIKeyRepository; +import zone.themcgamer.data.mysql.MySQLController; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.*; +import java.util.concurrent.TimeUnit; + +/** + * @author Braydon + */ +public class API { + private static final Gson gson = new GsonBuilder() + .registerTypeAdapter(IModel.class, new ModelSerializer()) + .serializeNulls() + .setPrettyPrinting() + .create(); + private static final List> models = new ArrayList<>(); + private static final Map> routes = new HashMap<>(); + private static final boolean requiresAuthentication = false; + + // Rate Limiting + private static final int rateLimit = 120; // The amount of requests a minute + private static final Cache requests = CacheBuilder.newBuilder() // The logged requests (ip, count) + .expireAfterWrite(1, TimeUnit.MINUTES) + .build(); + + public static void main(String[] args) { + // Initializing Redis + new JedisController().start(); + + // Initializing MySQL and Repositories + MySQLController mySQLController = new MySQLController(false); + AccountRepository accountRepository = new AccountRepository(mySQLController.getDataSource()); + + // Adding models + addModel(MinecraftServerModel.class); + addModel(NodeModel.class); + addModel(ServerGroupModel.class); + addModel(AccountModel.class); + addModel(StatusModel.class); + + // Adding the routes + addRoute(new ServersRoute()); + addRoute(new AccountRoute(accountRepository)); + addRoute(new StatusRoute()); + + // 404 Handling + Spark.notFound((request, response) -> { + response.type("application/json"); + HashMap map = new HashMap<>(); + map.put("success", "false"); + map.put("error", "Not found"); + + System.err.println("Requested url not found: " + request.pathInfo()); + + return gson.toJson(map); + }); + + // Handling the routes + APIKeyRepository apiKeyRepository = RedisRepository.getRepository(APIKeyRepository.class).orElse(null); + if (apiKeyRepository == null) + throw new NullPointerException(); + for (Map.Entry> entry : routes.entrySet()) { + for (Method method : entry.getValue()) { + RestPath restPath = method.getAnnotation(RestPath.class); + String path = "/" + restPath.version().getName() + restPath.path(); + System.out.println("Registered Path: " + path + " (accessLevel = " + restPath.accessLevel().name() + ")"); + Spark.get(path, (request, response) -> { + response.type("application/json"); + + JsonObject jsonObject = new JsonObject(); + try { + Integer requestCount = requests.getIfPresent(request.ip()); + if (requestCount == null) + requestCount = 0; + int remaining = Math.max(rateLimit - requestCount, 0); + + // Display the rate limit and the remaining request count using headers + response.header("x-ratelimit-limit", "" + rateLimit); + response.header("x-ratelimit-remaining", "" + remaining); + + // If the rate limit has been exceeded, set the status code to 429 (Too Many Requests) and display an error + if (++requestCount > rateLimit) { + System.out.println("Incoming request from \"" + request.ip() + "\" using path \"" + request.pathInfo() + "\" was rate limited"); + response.status(429); + throw new APIException("Rate limit exceeded"); + } + requests.put(request.ip(), requestCount); + + System.out.println("Handling incoming request from \"" + request.ip() + "\" using path \"" + request.pathInfo() + "\""); + + APIKey key = new APIKey(UUID.randomUUID().toString(), APIAccessLevel.STANDARD); + if (requiresAuthentication) { + String apiKey = request.headers("key"); + if (apiKey == null) + throw new APIException("Unauthorized"); + // Check if the provided API key is valid + Optional> keys = apiKeyRepository.lookup(apiKey); + if (keys.isEmpty() || (keys.get().isEmpty())) + throw new APIException("Unauthorized"); + key = keys.get().get(0); + if (restPath.accessLevel() == APIAccessLevel.DEV && (restPath.accessLevel() != key.getAccessLevel())) + throw new APIException("Unauthorized"); + } + // Checking if the request has the appropriate headers for the get route + if (restPath.headers().length > 0) { + for (String header : restPath.headers()) { + if (!request.headers().contains(header)) { + throw new APIException("Inappropriate headers"); + } + } + } + if (method.getParameterTypes().length != 3) + throw new APIException("Invalid route defined"); + Object object = method.invoke(entry.getKey(), request, response, key); + jsonObject.addProperty("success", "true"); + if (object instanceof IModel) { + if (models.contains(object.getClass())) + jsonObject.add("value", gson.toJsonTree(((IModel) object).toMap(), HashMap.class)); + else throw new APIException("Undefined model: " + object.toString()); + } else if (object instanceof ArrayList) + jsonObject.add("value", gson.toJsonTree(object, ArrayList.class)); + else if (object instanceof HashMap) + jsonObject.add("value", gson.toJsonTree(object, HashMap.class)); + else jsonObject.addProperty("value", object.toString()); + } catch (Throwable ex) { + if (ex instanceof InvocationTargetException) + ex = ex.getCause(); + String message = ex.getLocalizedMessage(); + if (message == null || (message.trim().isEmpty())) + message = ex.getClass().getSimpleName(); + jsonObject.addProperty("success", "false"); + jsonObject.addProperty("error", message); + if (!(ex instanceof APIException)) { + System.err.println("The route \"" + entry.getKey().getClass().getSimpleName() + "\" raised an exception:"); + ex.printStackTrace(); + } + } + return gson.toJson(jsonObject); + }); + } + } + } + + private static void addModel(Class modelClass) { + models.add(modelClass); + } + + private static void addRoute(Object object) { + List methods = routes.getOrDefault(object, new ArrayList<>()); + for (Method method : object.getClass().getMethods()) { + if (method.isAnnotationPresent(RestPath.class)) { + methods.add(method); + } + } + routes.put(object, methods); + } +} \ No newline at end of file diff --git a/api/src/main/java/zone/themcgamer/api/APIException.java b/api/src/main/java/zone/themcgamer/api/APIException.java new file mode 100644 index 0000000..04d3ba5 --- /dev/null +++ b/api/src/main/java/zone/themcgamer/api/APIException.java @@ -0,0 +1,25 @@ +package zone.themcgamer.api; + +/** + * @author Braydon + */ +public class APIException extends RuntimeException { + /** + * Constructs a new runtime exception with {@code null} as its + * detail message. The cause is not initialized, and may subsequently be + * initialized by a call to {@link #initCause}. + */ + public APIException() {} + + /** + * Constructs a new runtime exception with the specified detail message. + * The cause is not initialized, and may subsequently be initialized by a + * call to {@link #initCause}. + * + * @param message the detail message. The detail message is saved for + * later retrieval by the {@link #getMessage()} method. + */ + public APIException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/api/src/main/java/zone/themcgamer/api/APIVersion.java b/api/src/main/java/zone/themcgamer/api/APIVersion.java new file mode 100644 index 0000000..6ccc0ed --- /dev/null +++ b/api/src/main/java/zone/themcgamer/api/APIVersion.java @@ -0,0 +1,14 @@ +package zone.themcgamer.api; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author Braydon + */ +@AllArgsConstructor @Getter +public enum APIVersion { + V1("v1"); + + private final String name; +} \ No newline at end of file diff --git a/api/src/main/java/zone/themcgamer/api/RestPath.java b/api/src/main/java/zone/themcgamer/api/RestPath.java new file mode 100644 index 0000000..ab8b135 --- /dev/null +++ b/api/src/main/java/zone/themcgamer/api/RestPath.java @@ -0,0 +1,23 @@ +package zone.themcgamer.api; + +import zone.themcgamer.data.APIAccessLevel; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Braydon + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface RestPath { + String path(); + + APIVersion version(); + + APIAccessLevel accessLevel() default APIAccessLevel.STANDARD; + + String[] headers() default {}; +} \ No newline at end of file diff --git a/api/src/main/java/zone/themcgamer/api/model/IModel.java b/api/src/main/java/zone/themcgamer/api/model/IModel.java new file mode 100644 index 0000000..0dbd7b5 --- /dev/null +++ b/api/src/main/java/zone/themcgamer/api/model/IModel.java @@ -0,0 +1,10 @@ +package zone.themcgamer.api.model; + +import java.util.HashMap; + +/** + * @author Braydon + */ +public interface IModel { + HashMap toMap(); +} \ No newline at end of file diff --git a/api/src/main/java/zone/themcgamer/api/model/ModelSerializer.java b/api/src/main/java/zone/themcgamer/api/model/ModelSerializer.java new file mode 100644 index 0000000..3f33b54 --- /dev/null +++ b/api/src/main/java/zone/themcgamer/api/model/ModelSerializer.java @@ -0,0 +1,41 @@ +package zone.themcgamer.api.model; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +import java.lang.reflect.Type; +import java.util.Map; + +/** + * @author Braydon + */ +public class ModelSerializer implements JsonSerializer { + /** + * Gson invokes this call-back method during serialization when it encounters a field of the + * specified type. + * + *

In the implementation of this call-back method, you should consider invoking + * {@link JsonSerializationContext#serialize(Object, Type)} method to create JsonElements for any + * non-trivial field of the {@code iModel} object. However, you should never invoke it on the + * {@code iModel} object itself since that will cause an infinite loop (Gson will call your + * call-back method again).

+ * + * @param iModel the object that needs to be converted to Json. + * @param typeOfSrc the actual type (fully genericized version) of the source object. + * @param context + * @return a JsonElement corresponding to the specified object. + */ + @Override + public JsonElement serialize(IModel iModel, Type typeOfSrc, JsonSerializationContext context) { + JsonObject object = new JsonObject(); + for (Map.Entry entry : iModel.toMap().entrySet()) { + Object value = entry.getValue(); + if (value instanceof Enum) + value = ((Enum) value).name(); + object.addProperty(entry.getKey(), value.toString()); + } + return object; + } +} \ No newline at end of file diff --git a/api/src/main/java/zone/themcgamer/api/model/impl/AccountModel.java b/api/src/main/java/zone/themcgamer/api/model/impl/AccountModel.java new file mode 100644 index 0000000..afc6f2b --- /dev/null +++ b/api/src/main/java/zone/themcgamer/api/model/impl/AccountModel.java @@ -0,0 +1,46 @@ +package zone.themcgamer.api.model.impl; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import zone.themcgamer.api.model.IModel; +import zone.themcgamer.data.Rank; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * @author Braydon + */ +@AllArgsConstructor @Setter @Getter @ToString +public class AccountModel implements IModel { + private final int id; + private final UUID uuid; + private final String name; + private final Rank primaryRank; + private final Rank[] secondaryRanks; + private final double gold, gems; + private String encryptedIpAddress; + private final long firstLogin, lastLogin; + private long timeCached; + + @Override + public HashMap toMap() { + return new HashMap<>() {{ + put("id", id); + put("uuid", uuid); + put("name", name); + put("primaryRank", primaryRank.name()); + put("secondaryRanks", Arrays.stream(secondaryRanks).map(Rank::name).collect(Collectors.joining(", "))); + put("gold", gold); + put("gems", gems); + put("encryptedIpAddress", encryptedIpAddress); + put("firstLogin", firstLogin); + put("lastLogin", lastLogin); + put("timeCached", timeCached); + }}; + } +} \ No newline at end of file diff --git a/api/src/main/java/zone/themcgamer/api/model/impl/MinecraftServerModel.java b/api/src/main/java/zone/themcgamer/api/model/impl/MinecraftServerModel.java new file mode 100644 index 0000000..28c2f7f --- /dev/null +++ b/api/src/main/java/zone/themcgamer/api/model/impl/MinecraftServerModel.java @@ -0,0 +1,90 @@ +package zone.themcgamer.api.model.impl; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import zone.themcgamer.api.model.IModel; +import zone.themcgamer.data.jedis.data.server.MinecraftServer; +import zone.themcgamer.data.jedis.data.server.ServerState; + +import java.util.HashMap; +import java.util.UUID; + +/** + * @author Braydon + */ +@AllArgsConstructor @Setter @Getter @ToString +public class MinecraftServerModel implements IModel { + private final String id; + private final int numericId; + private final String name; + private final NodeModel node; + private final ServerGroupModel group; + private final String address; + private final long port; + + private final int usedRam, maxRam; + private final ServerState state; + private final long lastStateChange; + + private final int online, maxPlayers; + private final double tps; + private final UUID host; + private final String game; + + private final String metaData; + private final long created, lastHeartbeat; + + @Override + public HashMap toMap() { + return new HashMap<>() {{ + put("id", id); + put("numericId", numericId); + put("name", name); + put("node", node == null ? null : node.toMap()); + put("group", group); + put("address", address); + put("port", port); + put("usedRam", usedRam); + put("maxRam", maxRam); + put("state", state.name()); + put("lastStateChange", lastStateChange); + put("online", online); + put("maxPlayers", maxPlayers); + put("tps", tps); + put("host", host); + put("game", game); + put("metaData", metaData); + put("lastHeartbeat", lastHeartbeat); + }}; + } + + public static MinecraftServerModel fromMinecraftServer(MinecraftServer minecraftServer) { + if (minecraftServer == null) + return null; + return new MinecraftServerModel( + minecraftServer.getId(), + minecraftServer.getNumericId(), + minecraftServer.getName(), + NodeModel.fromNode(minecraftServer.getNode()), + ServerGroupModel.fromServerGroup(minecraftServer.getGroup()), + minecraftServer.getAddress(), + minecraftServer.getPort(), + + minecraftServer.getUsedRam(), + minecraftServer.getMaxRam(), + minecraftServer.getState(), + minecraftServer.getLastStateChange(), + + minecraftServer.getOnline(), + minecraftServer.getMaxPlayers(), + minecraftServer.getTps(), + minecraftServer.getHost(), + minecraftServer.getGame(), + minecraftServer.getMetaData(), + minecraftServer.getCreated(), + minecraftServer.getLastHeartbeat() + ); + } +} \ No newline at end of file diff --git a/api/src/main/java/zone/themcgamer/api/model/impl/NodeModel.java b/api/src/main/java/zone/themcgamer/api/model/impl/NodeModel.java new file mode 100644 index 0000000..743cbc6 --- /dev/null +++ b/api/src/main/java/zone/themcgamer/api/model/impl/NodeModel.java @@ -0,0 +1,36 @@ +package zone.themcgamer.api.model.impl; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; +import zone.themcgamer.api.model.IModel; +import zone.themcgamer.data.jedis.data.Node; + +import java.util.HashMap; + +/** + * @author Braydon + */ +@AllArgsConstructor @Getter @ToString +public class NodeModel implements IModel { + private final String name, address, portRange; + + @Override + public HashMap toMap() { + return new HashMap<>() {{ + put("name", name); + put("address", address); + put("portRange", portRange); + }}; + } + + public static NodeModel fromNode(Node node) { + if (node == null) + return null; + return new NodeModel( + node.getName(), + node.getAddress(), + node.getPortRange() + ); + } +} \ No newline at end of file diff --git a/api/src/main/java/zone/themcgamer/api/model/impl/ServerGroupModel.java b/api/src/main/java/zone/themcgamer/api/model/impl/ServerGroupModel.java new file mode 100644 index 0000000..486da0d --- /dev/null +++ b/api/src/main/java/zone/themcgamer/api/model/impl/ServerGroupModel.java @@ -0,0 +1,70 @@ +package zone.themcgamer.api.model.impl; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import zone.themcgamer.api.model.IModel; +import zone.themcgamer.data.jedis.data.ServerGroup; + +import java.util.HashMap; +import java.util.UUID; + +/** + * @author Braydon + */ +@RequiredArgsConstructor @Setter @Getter @ToString +public class ServerGroupModel implements IModel { + private final String name; + private final long memoryPerServer; + private final String serverJar, templatePath, pluginJarName, worldPath, startupScript, privateAddress; + private final UUID host; + private final String game; + private final int minPlayers, maxPlayers, minServers, maxServers; + private final boolean kingdom, staticGroup; + + @Override + public HashMap toMap() { + return new HashMap<>() {{ + put("name", name); + put("memoryPerServer", memoryPerServer); + put("serverJar", serverJar); + put("templatePath", templatePath); + put("pluginJarName", pluginJarName); + put("worldPath", worldPath); + put("startupScript", startupScript); + put("privateAddress", privateAddress); + put("host", host); + put("game", game); + put("minPlayers", minPlayers); + put("maxPlayers", maxPlayers); + put("minServers", minServers); + put("maxServers", maxServers); + put("kingdom", kingdom); + put("staticGroup", staticGroup); + }}; + } + + public static ServerGroupModel fromServerGroup(ServerGroup serverGroup) { + if (serverGroup == null) + return null; + return new ServerGroupModel( + serverGroup.getName(), + serverGroup.getMemoryPerServer(), + serverGroup.getServerJar(), + serverGroup.getTemplatePath(), + serverGroup.getPluginJarName(), + serverGroup.getWorldPath(), + serverGroup.getStartupScript(), + serverGroup.getPrivateAddress(), + serverGroup.getHost(), + serverGroup.getGame(), + serverGroup.getMinPlayers(), + serverGroup.getMaxPlayers(), + serverGroup.getMinServers(), + serverGroup.getMaxServers(), + serverGroup.isKingdom(), + serverGroup.isStaticGroup() + ); + } +} \ No newline at end of file diff --git a/api/src/main/java/zone/themcgamer/api/model/impl/StatusModel.java b/api/src/main/java/zone/themcgamer/api/model/impl/StatusModel.java new file mode 100644 index 0000000..cb731ad --- /dev/null +++ b/api/src/main/java/zone/themcgamer/api/model/impl/StatusModel.java @@ -0,0 +1,31 @@ +package zone.themcgamer.api.model.impl; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import zone.themcgamer.api.model.IModel; + +import java.util.HashMap; +import java.util.UUID; + +/** + * @author Braydon + */ +@AllArgsConstructor @Setter @Getter @ToString +public class StatusModel implements IModel { + private final UUID uuid; + private final String playerName; + private String server; + private final long timeJoined; + + @Override + public HashMap toMap() { + return new HashMap<>() {{ + put("uuid", uuid); + put("name", playerName); + put("server", server); + put("timeJoined", timeJoined); + }}; + } +} \ No newline at end of file diff --git a/api/src/main/java/zone/themcgamer/api/repository/AccountRepository.java b/api/src/main/java/zone/themcgamer/api/repository/AccountRepository.java new file mode 100644 index 0000000..d6e60da --- /dev/null +++ b/api/src/main/java/zone/themcgamer/api/repository/AccountRepository.java @@ -0,0 +1,75 @@ +package zone.themcgamer.api.repository; + +import com.zaxxer.hikari.HikariDataSource; +import zone.themcgamer.api.model.impl.AccountModel; +import zone.themcgamer.data.Rank; +import zone.themcgamer.data.mysql.data.column.Column; +import zone.themcgamer.data.mysql.data.column.impl.StringColumn; +import zone.themcgamer.data.mysql.repository.MySQLRepository; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.Objects; +import java.util.UUID; + +/** + * @author Braydon + */ +public class AccountRepository extends MySQLRepository { + private static final String SELECT_ACCOUNT = "SELECT * FROM `accounts` WHERE `uuid` = ? LIMIT 1"; + + public AccountRepository(HikariDataSource dataSource) { + super(dataSource); + } + + public AccountModel getAccount(UUID uuid) { + AccountModel[] model = new AccountModel[] { null }; + executeQuery(SELECT_ACCOUNT, new Column[] { + new StringColumn("uuid", uuid.toString()) + }, resultSet -> { + try { + if (resultSet.next()) + model[0] = constructAccount(uuid, resultSet); + } catch (SQLException ex) { + ex.printStackTrace(); + } + }); + return model[0]; + } + + /** + * Construct a {@link AccountModel} from the given parameters + * @param accountId the account id + * @param uuid the uuid + * @param name the name + * @param resultSet the result set + * @param ipAddress the ip address + * @param encryptedIpAddress the encrypted ip address + * @param lastLogin the last login + * @return the account + */ + private AccountModel constructAccount(UUID uuid, ResultSet resultSet) { + try { + Rank[] secondaryRanks = Arrays.stream(resultSet.getString("secondaryRanks") + .split(",")).map(rankName -> Rank.lookup(rankName).orElse(null)) + .filter(Objects::nonNull).toArray(Rank[]::new); + return new AccountModel( + resultSet.getInt("id"), + uuid, + resultSet.getString("name"), + Rank.lookup(resultSet.getString("primaryRank")).orElse(Rank.DEFAULT), + secondaryRanks, + resultSet.getInt("gold"), + resultSet.getInt("gems"), + resultSet.getString("ipAddress"), + resultSet.getLong("firstLogin"), + resultSet.getLong("lastLogin"), + -1L + ); + } catch (SQLException ex) { + ex.printStackTrace(); + } + return null; + } +} \ No newline at end of file diff --git a/api/src/main/java/zone/themcgamer/api/route/AccountRoute.java b/api/src/main/java/zone/themcgamer/api/route/AccountRoute.java new file mode 100644 index 0000000..b9d4492 --- /dev/null +++ b/api/src/main/java/zone/themcgamer/api/route/AccountRoute.java @@ -0,0 +1,47 @@ +package zone.themcgamer.api.route; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import lombok.AllArgsConstructor; +import spark.Request; +import spark.Response; +import zone.themcgamer.api.APIException; +import zone.themcgamer.api.APIVersion; +import zone.themcgamer.api.RestPath; +import zone.themcgamer.api.model.impl.AccountModel; +import zone.themcgamer.api.repository.AccountRepository; +import zone.themcgamer.common.MiscUtils; +import zone.themcgamer.data.APIAccessLevel; +import zone.themcgamer.data.jedis.data.APIKey; + +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +/** + * @author Braydon + */ +@AllArgsConstructor +public class AccountRoute { + // Account model cache for players that were looked up via the account route + public static final Cache CACHE = CacheBuilder.newBuilder() + .expireAfterWrite(10, TimeUnit.MINUTES) + .build(); + + private final AccountRepository accountRepository; + + @RestPath(path = "/account/:uuid", version = APIVersion.V1) + public AccountModel get(Request request, Response response, APIKey apiKey) throws APIException { + UUID uuid = MiscUtils.getUuid(request.params(":uuid")); + if (uuid == null) + throw new APIException("Invalid UUID"); + AccountModel account = CACHE.getIfPresent(uuid); + if (account == null) { + account = accountRepository.getAccount(uuid); + account.setTimeCached(System.currentTimeMillis()); + CACHE.put(uuid, account); + } + if (apiKey.getAccessLevel() == APIAccessLevel.STANDARD) + account.setEncryptedIpAddress("Unauthorized"); + return account; + } +} \ No newline at end of file diff --git a/api/src/main/java/zone/themcgamer/api/route/ServersRoute.java b/api/src/main/java/zone/themcgamer/api/route/ServersRoute.java new file mode 100644 index 0000000..88cdc31 --- /dev/null +++ b/api/src/main/java/zone/themcgamer/api/route/ServersRoute.java @@ -0,0 +1,79 @@ +package zone.themcgamer.api.route; + +import spark.Request; +import spark.Response; +import zone.themcgamer.api.APIException; +import zone.themcgamer.api.APIVersion; +import zone.themcgamer.api.RestPath; +import zone.themcgamer.api.model.impl.MinecraftServerModel; +import zone.themcgamer.api.model.impl.ServerGroupModel; +import zone.themcgamer.data.APIAccessLevel; +import zone.themcgamer.data.jedis.data.APIKey; +import zone.themcgamer.data.jedis.data.ServerGroup; +import zone.themcgamer.data.jedis.data.server.MinecraftServer; +import zone.themcgamer.data.jedis.repository.RedisRepository; +import zone.themcgamer.data.jedis.repository.impl.MinecraftServerRepository; +import zone.themcgamer.data.jedis.repository.impl.ServerGroupRepository; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * @author Braydon + */ +public class ServersRoute { + private final ServerGroupRepository serverGroupRepository; + private final MinecraftServerRepository minecraftServerRepository; + + public ServersRoute() { + serverGroupRepository = RedisRepository.getRepository(ServerGroupRepository.class).orElse(null); + minecraftServerRepository = RedisRepository.getRepository(MinecraftServerRepository.class).orElse(null); + } + + /* + Server Groups + */ + + @RestPath(path = "/serverGroups", version = APIVersion.V1, accessLevel = APIAccessLevel.DEV) + public List getGroups(Request request, Response response, APIKey apiKey) throws APIException { + List models = new ArrayList<>(); + for (ServerGroup serverGroup : serverGroupRepository.getCached()) + models.add(ServerGroupModel.fromServerGroup(serverGroup)); + return models; + } + + @RestPath(path = "/serverGroup/:name", version = APIVersion.V1, accessLevel = APIAccessLevel.DEV) + public ServerGroupModel getServerGroup(Request request, Response response, APIKey apiKey) throws APIException { + String name = request.params(":name"); + if (name == null || (name.trim().isEmpty())) + throw new APIException("Invalid Server Group"); + Optional optionalServerGroup = serverGroupRepository.lookup(name); + if (optionalServerGroup.isEmpty()) + throw new APIException("Server group not found"); + return ServerGroupModel.fromServerGroup(optionalServerGroup.get()); + } + + /* + Minecraft Servers + */ + + @RestPath(path = "/minecraftServers", version = APIVersion.V1, accessLevel = APIAccessLevel.DEV) + public List getMinecraftServers(Request request, Response response, APIKey apiKey) throws APIException { + List models = new ArrayList<>(); + for (MinecraftServer minecraftServer : minecraftServerRepository.getCached()) + models.add(MinecraftServerModel.fromMinecraftServer(minecraftServer)); + return models; + } + + @RestPath(path = "/minecraftServer/:id", version = APIVersion.V1, accessLevel = APIAccessLevel.DEV) + public MinecraftServerModel getMinecraftServer(Request request, Response response, APIKey apiKey) throws APIException { + String id = request.params(":id"); + if (id == null || (id.trim().isEmpty())) + throw new APIException("Invalid Minecraft Server"); + Optional optionalMinecraftServer = minecraftServerRepository.lookup(id); + if (optionalMinecraftServer.isEmpty()) + throw new APIException("Minecraft server not found"); + return MinecraftServerModel.fromMinecraftServer(optionalMinecraftServer.get()); + } +} \ No newline at end of file diff --git a/api/src/main/java/zone/themcgamer/api/route/StatusRoute.java b/api/src/main/java/zone/themcgamer/api/route/StatusRoute.java new file mode 100644 index 0000000..5244779 --- /dev/null +++ b/api/src/main/java/zone/themcgamer/api/route/StatusRoute.java @@ -0,0 +1,62 @@ +package zone.themcgamer.api.route; + +import spark.Request; +import spark.Response; +import zone.themcgamer.api.APIException; +import zone.themcgamer.api.APIVersion; +import zone.themcgamer.api.RestPath; +import zone.themcgamer.api.model.impl.StatusModel; +import zone.themcgamer.data.APIAccessLevel; +import zone.themcgamer.data.jedis.cache.CacheRepository; +import zone.themcgamer.data.jedis.cache.ICacheItem; +import zone.themcgamer.data.jedis.cache.ItemCacheType; +import zone.themcgamer.data.jedis.cache.impl.PlayerStatusCache; +import zone.themcgamer.data.jedis.data.APIKey; +import zone.themcgamer.data.jedis.repository.RedisRepository; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author Braydon + */ +public class StatusRoute { + private final CacheRepository repository; + + public StatusRoute() { + repository = RedisRepository.getRepository(CacheRepository.class).orElse(null); + } + + @RestPath(path = "/status", version = APIVersion.V1) + public Map getStatuses(Request request, Response response, APIKey apiKey) throws APIException { + List> statuses = repository.filter(cacheItem -> cacheItem.getType() == ItemCacheType.PLAYER_STATUS); + StringBuilder namesBuilder = new StringBuilder(); + for (ICacheItem status : statuses) + namesBuilder.append(((PlayerStatusCache) status).getPlayerName()).append(", "); + String names = namesBuilder.toString(); + if (!names.isEmpty()) + names = names.substring(0, names.length() - 2); + String finalNames = names; + return new HashMap<>() {{ + put("total", statuses.size()); + put("names", apiKey.getAccessLevel() == APIAccessLevel.STANDARD ? "Unauthorized" : finalNames); + }}; + } + + @RestPath(path = "/status/:name", version = APIVersion.V1) + public StatusModel getStatus(Request request, Response response, APIKey apiKey) throws APIException { + String name = request.params(":name"); + if (name == null || (name.trim().isEmpty() || name.length() > 16)) + throw new APIException("Invalid username"); + PlayerStatusCache statusCache = repository + .lookup(PlayerStatusCache.class, playerStatusCache -> playerStatusCache.getPlayerName().equals(name)) + .stream().findFirst().orElse(null); + if (statusCache == null) + throw new APIException("Player not found"); + StatusModel model = new StatusModel(statusCache.getUuid(), statusCache.getPlayerName(), statusCache.getServer(), statusCache.getTimeJoined()); + if (apiKey.getAccessLevel() == APIAccessLevel.STANDARD) + model.setServer("Unauthorized"); + return model; + } +} \ No newline at end of file diff --git a/api/src/main/resources/META-INF/MANIFEST.MF b/api/src/main/resources/META-INF/MANIFEST.MF new file mode 100644 index 0000000..9bd7e12 --- /dev/null +++ b/api/src/main/resources/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: zone.themcgamer.api.API + diff --git a/arcade/build.gradle.kts b/arcade/build.gradle.kts new file mode 100644 index 0000000..9ccdef2 --- /dev/null +++ b/arcade/build.gradle.kts @@ -0,0 +1,19 @@ +dependencies { + implementation(project(":core")) + compileOnly("com.destroystokyo:paperspigot:1.12.2") + implementation("com.github.cryptomorin:XSeries:7.8.0") +} + +tasks { + processResources { + val tokens = mapOf("version" to project.version) + from(sourceSets["main"].resources.srcDirs) { + filter("tokens" to tokens) + } + } + + shadowJar { + archiveFileName.set("${project.rootProject.name}-${project.name}-v${project.version}.jar") + destinationDir = file("$rootDir/output") + } +} \ No newline at end of file diff --git a/arcade/src/main/java/zone/themcgamer/arcade/Arcade.java b/arcade/src/main/java/zone/themcgamer/arcade/Arcade.java new file mode 100644 index 0000000..f7119a7 --- /dev/null +++ b/arcade/src/main/java/zone/themcgamer/arcade/Arcade.java @@ -0,0 +1,53 @@ +package zone.themcgamer.arcade; + +import lombok.Getter; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import zone.themcgamer.arcade.manager.ArcadeManager; +import zone.themcgamer.arcade.player.PlayerDataManager; +import zone.themcgamer.arcade.scoreboard.ArcadeScoreboard; +import zone.themcgamer.core.chat.ChatManager; +import zone.themcgamer.core.chat.component.IChatComponent; +import zone.themcgamer.core.chat.component.impl.BasicNameComponent; +import zone.themcgamer.core.chat.component.impl.BasicRankComponent; +import zone.themcgamer.core.common.MathUtils; +import zone.themcgamer.core.common.scoreboard.ScoreboardHandler; +import zone.themcgamer.core.plugin.MGZPlugin; +import zone.themcgamer.core.plugin.Startup; +import zone.themcgamer.core.world.MGZWorld; + +/** + * @author Braydon + */ +@Getter +public class Arcade extends MGZPlugin { + public static Arcade INSTANCE; + + private ArcadeManager arcadeManager; + private Location spawn; + + @Override + public void onEnable() { + super.onEnable(); + INSTANCE = this; + } + + @Startup + public void loadArcade() { + new PlayerDataManager(this); + arcadeManager = new ArcadeManager(this, traveller); + + new ScoreboardHandler(this, ArcadeScoreboard.class, 3L); + + new ChatManager(this, badSportSystem, new IChatComponent[] { + new BasicRankComponent(), + new BasicNameComponent() + }); + + MGZWorld world = MGZWorld.get(Bukkit.getWorlds().get(0)); + spawn = world.getDataPoint("SPAWN"); + if (spawn != null) + spawn.setYaw(MathUtils.getFacingYaw(spawn, world.getDataPoints("LOOK_AT"))); + else spawn = new Location(world.getWorld(), 0, 150, 0); + } +} \ No newline at end of file diff --git a/arcade/src/main/java/zone/themcgamer/arcade/commands/GameCommand.java b/arcade/src/main/java/zone/themcgamer/arcade/commands/GameCommand.java new file mode 100644 index 0000000..a8fee92 --- /dev/null +++ b/arcade/src/main/java/zone/themcgamer/arcade/commands/GameCommand.java @@ -0,0 +1,11 @@ +package zone.themcgamer.arcade.commands; + +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.core.command.help.HelpCommand; +import zone.themcgamer.data.Rank; + +public class GameCommand extends HelpCommand { + @Command(name = "game", aliases = {"arcade"}, description = "Game commands", ranks = { Rank.ADMIN }) + public void onCommand(CommandProvider command) {} +} diff --git a/arcade/src/main/java/zone/themcgamer/arcade/commands/arguments/GameForceMapCommand.java b/arcade/src/main/java/zone/themcgamer/arcade/commands/arguments/GameForceMapCommand.java new file mode 100644 index 0000000..e675168 --- /dev/null +++ b/arcade/src/main/java/zone/themcgamer/arcade/commands/arguments/GameForceMapCommand.java @@ -0,0 +1,18 @@ +package zone.themcgamer.arcade.commands.arguments; + +import lombok.RequiredArgsConstructor; +import org.bukkit.command.CommandSender; +import zone.themcgamer.arcade.game.Game; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.data.Rank; + +@RequiredArgsConstructor +public class GameForceMapCommand { + final Game game; + @Command(name = "game.forcemap", usage = "", description = "Force a map.", ranks = { Rank.ADMIN }) + public void onCommand(CommandProvider command) { + CommandSender sender = command.getSender(); + String[] args = command.getArgs(); + } +} diff --git a/arcade/src/main/java/zone/themcgamer/arcade/commands/arguments/GameHostCommand.java b/arcade/src/main/java/zone/themcgamer/arcade/commands/arguments/GameHostCommand.java new file mode 100644 index 0000000..1645efd --- /dev/null +++ b/arcade/src/main/java/zone/themcgamer/arcade/commands/arguments/GameHostCommand.java @@ -0,0 +1,16 @@ +package zone.themcgamer.arcade.commands.arguments; + +import org.bukkit.command.CommandSender; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.data.Rank; + +public class GameHostCommand { + @Command(name = "game.host", description = "Manage the game", ranks = { Rank.ADMIN }) + public void onCommand(CommandProvider command) { + CommandSender sender = command.getSender(); + String[] args = command.getArgs(); + + //TODO open gui to manage the game + } +} diff --git a/arcade/src/main/java/zone/themcgamer/arcade/commands/arguments/GameMaxPlayersCommand.java b/arcade/src/main/java/zone/themcgamer/arcade/commands/arguments/GameMaxPlayersCommand.java new file mode 100644 index 0000000..2a3325c --- /dev/null +++ b/arcade/src/main/java/zone/themcgamer/arcade/commands/arguments/GameMaxPlayersCommand.java @@ -0,0 +1,14 @@ +package zone.themcgamer.arcade.commands.arguments; + +import org.bukkit.command.CommandSender; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.data.Rank; + +public class GameMaxPlayersCommand { + @Command(name = "game.maxplayers", usage = "", description = "Set the max players.", ranks = { Rank.ADMIN }) + public void onCommand(CommandProvider command) { + CommandSender sender = command.getSender(); + String[] args = command.getArgs(); + } +} diff --git a/arcade/src/main/java/zone/themcgamer/arcade/commands/arguments/GameMinPlayersCommand.java b/arcade/src/main/java/zone/themcgamer/arcade/commands/arguments/GameMinPlayersCommand.java new file mode 100644 index 0000000..c1fbb4a --- /dev/null +++ b/arcade/src/main/java/zone/themcgamer/arcade/commands/arguments/GameMinPlayersCommand.java @@ -0,0 +1,14 @@ +package zone.themcgamer.arcade.commands.arguments; + +import org.bukkit.command.CommandSender; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.data.Rank; + +public class GameMinPlayersCommand { + @Command(name = "game.minplayers", usage = "", description = "Set the min players.", ranks = { Rank.ADMIN }) + public void onCommand(CommandProvider command) { + CommandSender sender = command.getSender(); + String[] args = command.getArgs(); + } +} diff --git a/arcade/src/main/java/zone/themcgamer/arcade/commands/arguments/GamePlayerTeamCommand.java b/arcade/src/main/java/zone/themcgamer/arcade/commands/arguments/GamePlayerTeamCommand.java new file mode 100644 index 0000000..262da69 --- /dev/null +++ b/arcade/src/main/java/zone/themcgamer/arcade/commands/arguments/GamePlayerTeamCommand.java @@ -0,0 +1,14 @@ +package zone.themcgamer.arcade.commands.arguments; + +import org.bukkit.command.CommandSender; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.data.Rank; + +public class GamePlayerTeamCommand { + @Command(name = "game.setteam", usage = " ", description = "Set a player to team", ranks = { Rank.ADMIN }) + public void onCommand(CommandProvider command) { + CommandSender sender = command.getSender(); + String[] args = command.getArgs(); + } +} diff --git a/arcade/src/main/java/zone/themcgamer/arcade/commands/arguments/GameStartCommand.java b/arcade/src/main/java/zone/themcgamer/arcade/commands/arguments/GameStartCommand.java new file mode 100644 index 0000000..7816bba --- /dev/null +++ b/arcade/src/main/java/zone/themcgamer/arcade/commands/arguments/GameStartCommand.java @@ -0,0 +1,39 @@ +package zone.themcgamer.arcade.commands.arguments; + +import lombok.RequiredArgsConstructor; +import org.bukkit.command.CommandSender; +import zone.themcgamer.arcade.game.GameState; +import zone.themcgamer.arcade.manager.ArcadeManager; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.data.Rank; + +import java.util.concurrent.TimeUnit; + +@RequiredArgsConstructor +public class GameStartCommand { + private final ArcadeManager arcadeManager; + + @Command(name = "game.start", description = "Force start the game", ranks = { Rank.ADMIN }) + public void onCommand(CommandProvider command) { + CommandSender sender = command.getSender(); + String[] args = command.getArgs(); + if (args.length < 1) { + sender.sendMessage(Style.error("Game", "Please use &6/" + command.getLabel() + " ")); + return; + } + if (arcadeManager.getState() != GameState.LOBBY) + sender.sendMessage(Style.main("Game", "The game is not in lobby state!")); + else { + try { + long time = Long.parseLong(args[0]); + arcadeManager.getMapVotingManager().startVoting(TimeUnit.SECONDS.toMillis(time)); + } catch (NumberFormatException ex) { + sender.sendMessage(Style.error("Game", "Invalid amount! Please use /" + command.getLabel() + " ")); + return; + } + arcadeManager.getMapVotingManager().startVoting(); + } + } +} diff --git a/arcade/src/main/java/zone/themcgamer/arcade/commands/arguments/GameStopCommand.java b/arcade/src/main/java/zone/themcgamer/arcade/commands/arguments/GameStopCommand.java new file mode 100644 index 0000000..631605c --- /dev/null +++ b/arcade/src/main/java/zone/themcgamer/arcade/commands/arguments/GameStopCommand.java @@ -0,0 +1,18 @@ +package zone.themcgamer.arcade.commands.arguments; + +import lombok.RequiredArgsConstructor; +import org.bukkit.command.CommandSender; +import zone.themcgamer.arcade.game.Game; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.data.Rank; + +@RequiredArgsConstructor +public class GameStopCommand { + final Game game; + @Command(name = "game.stop", description = "Force stop this game.", ranks = { Rank.ADMIN }) + public void onCommand(CommandProvider command) { + CommandSender sender = command.getSender(); + String[] args = command.getArgs(); + } +} diff --git a/arcade/src/main/java/zone/themcgamer/arcade/event/GameStateChangeEvent.java b/arcade/src/main/java/zone/themcgamer/arcade/event/GameStateChangeEvent.java new file mode 100644 index 0000000..6c1add6 --- /dev/null +++ b/arcade/src/main/java/zone/themcgamer/arcade/event/GameStateChangeEvent.java @@ -0,0 +1,17 @@ +package zone.themcgamer.arcade.event; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import zone.themcgamer.arcade.game.Game; +import zone.themcgamer.arcade.game.GameState; +import zone.themcgamer.core.common.WrappedBukkitEvent; + +/** + * @author Braydon + * @implNote This event is called when the {@link GameState} is changed + */ +@AllArgsConstructor @Getter +public class GameStateChangeEvent extends WrappedBukkitEvent { + private final Game game; + private final GameState from, to; +} \ No newline at end of file diff --git a/arcade/src/main/java/zone/themcgamer/arcade/game/Game.java b/arcade/src/main/java/zone/themcgamer/arcade/game/Game.java new file mode 100644 index 0000000..6e722f4 --- /dev/null +++ b/arcade/src/main/java/zone/themcgamer/arcade/game/Game.java @@ -0,0 +1,47 @@ +package zone.themcgamer.arcade.game; + +import lombok.Getter; +import lombok.Setter; +import org.bukkit.ChatColor; +import org.bukkit.entity.Player; +import org.bukkit.event.Listener; +import zone.themcgamer.arcade.player.GamePlayer; +import zone.themcgamer.arcade.team.Team; +import zone.themcgamer.core.game.MGZGame; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * @author Braydon + * @implNote This class represents a game + */ +@Setter @Getter +public abstract class Game implements Listener { + private final MGZGame mgzGame; + private final Team[] teams; + + // Game Flags + protected boolean joinMessages = true; + protected boolean quitMessages = true; + protected boolean blockPlace; + protected boolean blockBreak; + + // Data + private long started; + private final Set players = new HashSet<>(); + + public abstract List getScoreboard(GamePlayer gamePlayer, Player player); + + public Game(MGZGame mgzGame) { + this(mgzGame, null); + } + + public Game(MGZGame mgzGame, Team[] teams) { + this.mgzGame = mgzGame; + if (teams == null || (teams.length <= 0)) + teams = new Team[] { new Team("PLAYERS", ChatColor.WHITE, true) }; + this.teams = teams; + } +} \ No newline at end of file diff --git a/arcade/src/main/java/zone/themcgamer/arcade/game/GameManager.java b/arcade/src/main/java/zone/themcgamer/arcade/game/GameManager.java new file mode 100644 index 0000000..92d5327 --- /dev/null +++ b/arcade/src/main/java/zone/themcgamer/arcade/game/GameManager.java @@ -0,0 +1,24 @@ +package zone.themcgamer.arcade.game; + +import lombok.Getter; +import org.bukkit.plugin.java.JavaPlugin; +import zone.themcgamer.arcade.game.impl.TheBridgeGame; +import zone.themcgamer.core.module.Module; +import zone.themcgamer.core.module.ModuleInfo; + +import java.util.Arrays; +import java.util.List; + +/** + * @author Braydon + */ +@ModuleInfo(name = "Game Manager") @Getter +public class GameManager extends Module { + private final List games = Arrays.asList( + new TheBridgeGame() + ); + + public GameManager(JavaPlugin plugin) { + super(plugin); + } +} \ No newline at end of file diff --git a/arcade/src/main/java/zone/themcgamer/arcade/game/GameState.java b/arcade/src/main/java/zone/themcgamer/arcade/game/GameState.java new file mode 100644 index 0000000..bef164a --- /dev/null +++ b/arcade/src/main/java/zone/themcgamer/arcade/game/GameState.java @@ -0,0 +1,18 @@ +package zone.themcgamer.arcade.game; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author Braydon + * @implNote The state of the currently running game + */ +@AllArgsConstructor @Getter +public enum GameState { + LOBBY("§aWaiting..."), + STARTING("§6Starting"), + PLAYING("§cPlaying"), + ENDING("§9Ending"); + + private final String displayName; +} \ No newline at end of file diff --git a/arcade/src/main/java/zone/themcgamer/arcade/game/impl/TheBridgeGame.java b/arcade/src/main/java/zone/themcgamer/arcade/game/impl/TheBridgeGame.java new file mode 100644 index 0000000..a2052df --- /dev/null +++ b/arcade/src/main/java/zone/themcgamer/arcade/game/impl/TheBridgeGame.java @@ -0,0 +1,41 @@ +package zone.themcgamer.arcade.game.impl; + +import org.bukkit.ChatColor; +import org.bukkit.entity.Player; +import zone.themcgamer.arcade.game.Game; +import zone.themcgamer.arcade.player.GamePlayer; +import zone.themcgamer.arcade.team.Team; +import zone.themcgamer.core.game.MGZGame; + +import java.util.Arrays; +import java.util.List; + +/** + * @author Braydon + */ +public class TheBridgeGame extends Game { + public TheBridgeGame() { + super(MGZGame.THE_BRIDGE, new Team[] { + new Team("Red", ChatColor.RED, false), + new Team("Blue", ChatColor.BLUE, false) + }); + blockBreak = true; + blockPlace = true; + } + + @Override + public List getScoreboard(GamePlayer gamePlayer, Player player) { + return Arrays.asList( + "&b&lGame Duration", + "§e2:50", + "", + "§c§lRED", + "§fPlayers: §c0", + "§fNexus: §c100", + "", + "§9§lBLUE", + "§fPlayers: §90", + "§fNexus: §9100" + ); + } +} \ No newline at end of file diff --git a/arcade/src/main/java/zone/themcgamer/arcade/manager/ArcadeManager.java b/arcade/src/main/java/zone/themcgamer/arcade/manager/ArcadeManager.java new file mode 100644 index 0000000..7004342 --- /dev/null +++ b/arcade/src/main/java/zone/themcgamer/arcade/manager/ArcadeManager.java @@ -0,0 +1,300 @@ +package zone.themcgamer.arcade.manager; + +import com.cryptomorin.xseries.XSound; +import lombok.Getter; +import org.apache.commons.io.FileUtils; +import org.bukkit.*; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerLoginEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.Nullable; +import zone.themcgamer.arcade.Arcade; +import zone.themcgamer.arcade.commands.GameCommand; +import zone.themcgamer.arcade.commands.arguments.*; +import zone.themcgamer.arcade.event.GameStateChangeEvent; +import zone.themcgamer.arcade.game.Game; +import zone.themcgamer.arcade.game.GameManager; +import zone.themcgamer.arcade.game.GameState; +import zone.themcgamer.arcade.map.GameMap; +import zone.themcgamer.arcade.map.MapManager; +import zone.themcgamer.arcade.map.MapVotingManager; +import zone.themcgamer.arcade.map.event.MapVoteWinEvent; +import zone.themcgamer.arcade.player.GamePlayer; +import zone.themcgamer.core.account.Account; +import zone.themcgamer.core.account.AccountManager; +import zone.themcgamer.core.common.PlayerUtils; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.core.common.scheduler.ScheduleType; +import zone.themcgamer.core.common.scheduler.event.SchedulerEvent; +import zone.themcgamer.core.module.Module; +import zone.themcgamer.core.module.ModuleInfo; +import zone.themcgamer.core.traveller.ServerTraveller; +import zone.themcgamer.core.world.MGZWorld; + +import java.io.File; +import java.io.IOException; +import java.util.Optional; + +/** + * @author Braydon + */ +@ModuleInfo(name = "Arcade Manager") @Getter +public class ArcadeManager extends Module { + private static final int DEFAULT_COUNTDOWN = 10; + + private final GameManager gameManager; + + private Game game; + private GameState state = GameState.LOBBY; + private int countdown = -1; + + private GameMap map; + + private boolean mapsLoaded; + private MapVotingManager mapVotingManager; + + public ArcadeManager(JavaPlugin plugin, ServerTraveller traveller) { + super(plugin); + new LobbyManager(plugin, this, traveller); + gameManager = new GameManager(plugin); + game = gameManager.getGames().get(0); + MapManager mapManager = new MapManager(plugin); + mapManager.withConsumer(future -> { + future.whenComplete((maps, throwable) -> { + mapsLoaded = true; + mapVotingManager = new MapVotingManager(plugin, this, mapManager); + }); + }); + registerCommand(new GameCommand()); + registerCommand(new GameForceMapCommand(game)); + registerCommand(new GameHostCommand()); + registerCommand(new GameMaxPlayersCommand()); + registerCommand(new GameMinPlayersCommand()); + registerCommand(new GamePlayerTeamCommand()); + registerCommand(new GameStartCommand(this)); + registerCommand(new GameStopCommand(game)); + } + + @EventHandler + private void onLogin(PlayerLoginEvent event) { + if (!mapsLoaded) + event.disallow(PlayerLoginEvent.Result.KICK_OTHER, "§cPlease wait whilst we load the maps..."); + } + + @EventHandler + private void handleCountdown(SchedulerEvent event) { + if (countdown == -1) + return; + ChatColor color = ChatColor.GREEN; + switch (countdown) { + case 4: { + color = ChatColor.YELLOW; + break; + } + case 3: { + color = ChatColor.GOLD; + break; + } + case 2: { + color = ChatColor.RED; + break; + } + case 1: { + color = ChatColor.DARK_RED; + break; + } + } + if (event.getType() == ScheduleType.SECOND) { + if (Bukkit.getOnlinePlayers().size() < game.getMgzGame().getMinPlayers()) { + countdown = -1; + setState(GameState.LOBBY); + Bukkit.broadcastMessage(Style.error(game.getMgzGame().getName(), "§cCountdown stopped, there are not enough players!")); + return; + } + if (countdown > 0) { + for (Player player : Bukkit.getOnlinePlayers()) + player.sendActionBar("§aGame starting in §f" + color + countdown + "§a..."); + } + if (countdown % 10 == 0 || countdown <= 5) { + if (countdown <= 0) { + startGame(); + return; + } + float pitch = (float) countdown / 2f; + for (Player player : Bukkit.getOnlinePlayers()) { + player.playSound(player.getEyeLocation(), XSound.ENTITY_PLAYER_LEVELUP.parseSound(), 0.9f, pitch); + player.sendMessage(Style.main(game.getMgzGame().getName(), "Game starting in §f" + color + countdown + " second" + (countdown == 1 ? "" : "s") + "§7...")); + //player.sendTitle((countdown == 5 ? color + "\u277A" : (countdown == 4 ? color + "\u2779" : (countdown == 3 ? color + "\u2778" : (countdown == 2 ? color + "\u2777" : (countdown == 1 ? color + "\u2776" : ""))))), "",5,5,5); + } + } + countdown--; + } + } + + @EventHandler + private void handleActionBar(SchedulerEvent event) { + if (event.getType() == ScheduleType.SECOND) { + if (state != GameState.LOBBY) + return; + String actionBar = "&aWaiting for &b" + Math.abs(game.getMgzGame().getMaxPlayers() - Bukkit.getOnlinePlayers().size()) + " &amore players..."; + if (mapVotingManager != null && (mapVotingManager.isVoting())) { + actionBar = "&a&lPlease vote a map!"; + //TODO 2 colors animation for the vote map just do something you like + } + for (Player player : Bukkit.getOnlinePlayers()) { + player.sendActionBar(Style.color(actionBar)); + } + } + } + + @EventHandler + private void onMapVoteWin(MapVoteWinEvent event) { + if (state != GameState.LOBBY) + return; + setMap(event.getMap()); + startCountdown(DEFAULT_COUNTDOWN); + } + + @EventHandler + private void onJoin(PlayerJoinEvent event) { + Player player = event.getPlayer(); + PlayerUtils.reset(player, true, true, GameMode.SURVIVAL); + if (state == GameState.LOBBY || state == GameState.STARTING) + player.teleport(Arcade.INSTANCE.getSpawn()); + else { + // TODO: 1/27/21 teleport the player to the game map + } + + Optional optionalAccount = AccountManager.fromCache(player.getUniqueId()); + if (optionalAccount.isPresent() && game.isJoinMessages()) + event.setJoinMessage("§a§lJoin §8» §7" + optionalAccount.get().getDisplayName() + " §7has joined the game!"); + else event.setJoinMessage(null); + } + + @EventHandler(priority = EventPriority.LOWEST) + private void onQuit(PlayerQuitEvent event) { + Player player = event.getPlayer(); + Optional optionalAccount = AccountManager.fromCache(player.getUniqueId()); + if (optionalAccount.isPresent() && game.isQuitMessages()) + event.setQuitMessage("§4§lQuit §8» §7" + optionalAccount.get().getDisplayName() + " §7left the game!"); + else event.setQuitMessage(null); + } + + public void setMap(@Nullable MGZWorld mgzWorld) { + if (state != GameState.LOBBY) + throw new IllegalStateException("The map cannot be updated in this state: " + state.name()); + if (map != null) { + Bukkit.unloadWorld(map.getBukkitWorld(), true); + FileUtils.deleteQuietly(map.getBukkitWorld().getWorldFolder()); + } + if (mgzWorld == null) + return; + try { + FileUtils.copyDirectory(mgzWorld.getDataFile().getParentFile(), new File(mgzWorld.getName())); + + WorldCreator creator = new WorldCreator(mgzWorld.getName()); + creator.environment(World.Environment.NORMAL); + creator.type(WorldType.FLAT); + if (mgzWorld.getPreset() != null) + creator.generatorSettings(mgzWorld.getPreset()); + creator.generateStructures(false); + World world = creator.createWorld(); + + long time = 6000L; + if (mgzWorld.getName().toLowerCase().contains("christmas")) + time = 12000L; + else if (mgzWorld.getName().toLowerCase().contains("halloween")) + time = 17000L; + world.setTime(time); + world.setThundering(false); + world.setStorm(false); + world.setSpawnLocation(0, 150, 0); + world.setGameRuleValue("randomTickSpeed", "0"); + world.setGameRuleValue("doDaylightCycle", "false"); + world.setGameRuleValue("showDeathMessages", "false"); + world.setGameRuleValue("doFireTick", "false"); + world.setGameRuleValue("mobGriefing", "false"); + world.setGameRuleValue("doMobLoot", "false"); + world.setGameRuleValue("doMobSpawning", "false"); + + map = new GameMap(mgzWorld, world); + } catch (IOException ex) { + ex.printStackTrace(); + } + } + + /** + * Set the current game to the provided {@link Game} + * @param game the game to set + */ + public void setGame(Game game) { + if (state != GameState.LOBBY) + stopGame(); + this.game = game; + setState(GameState.LOBBY); + } + + /** + * Start the game instantly + */ + public void startCountdown() { + startCountdown(0); + } + + /** + * Start the game with the given countdown + * @param countdown the countdown + */ + public void startCountdown(int countdown) { + if (state != GameState.LOBBY) + return; + setState(GameState.STARTING); + this.countdown = countdown; + if (countdown <= 0) + startGame(); + } + + public void startGame() { + if (state == GameState.PLAYING || state == GameState.ENDING) + throw new IllegalStateException("The game is already in a running state: " + state.name()); + setState(GameState.PLAYING); + countdown = -1; + + // Setting up the game + game.setStarted(System.currentTimeMillis()); + + for (Player player : Bukkit.getOnlinePlayers()) { + // TODO: 1/31/21 check if player is a staff member and is vanished + + PlayerUtils.reset(player, true, true, GameMode.SURVIVAL); + + GamePlayer gamePlayer = GamePlayer.getPlayer(player.getUniqueId()); + + // TODO: 1/31/21 team calculations + + player.teleport(map.getBukkitWorld().getSpawnLocation()); + } + } + + /** + * Stop the currently running game + */ + public void stopGame() { + if (state != GameState.PLAYING) + return; + setState(GameState.LOBBY); + } + + /** + * Set the game state to the given {@link GameState} + * @param state the state to set + */ + private void setState(GameState state) { + Bukkit.getPluginManager().callEvent(new GameStateChangeEvent(game, this.state, state)); + this.state = state; + } +} \ No newline at end of file diff --git a/arcade/src/main/java/zone/themcgamer/arcade/manager/KingdomManager.java b/arcade/src/main/java/zone/themcgamer/arcade/manager/KingdomManager.java new file mode 100644 index 0000000..c359377 --- /dev/null +++ b/arcade/src/main/java/zone/themcgamer/arcade/manager/KingdomManager.java @@ -0,0 +1,15 @@ +package zone.themcgamer.arcade.manager; + +import org.bukkit.plugin.java.JavaPlugin; +import zone.themcgamer.core.module.Module; +import zone.themcgamer.core.module.ModuleInfo; + +/** + * @author Braydon + */ +@ModuleInfo(name = "Arcade Kingdom Manager") +public class KingdomManager extends Module { + public KingdomManager(JavaPlugin plugin) { + super(plugin); + } +} \ No newline at end of file diff --git a/arcade/src/main/java/zone/themcgamer/arcade/manager/LobbyManager.java b/arcade/src/main/java/zone/themcgamer/arcade/manager/LobbyManager.java new file mode 100644 index 0000000..5e066c0 --- /dev/null +++ b/arcade/src/main/java/zone/themcgamer/arcade/manager/LobbyManager.java @@ -0,0 +1,233 @@ +package zone.themcgamer.arcade.manager; + +import com.cryptomorin.xseries.XMaterial; +import org.bukkit.GameMode; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.block.Action; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.block.LeavesDecayEvent; +import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.event.entity.ExplosionPrimeEvent; +import org.bukkit.event.entity.FoodLevelChangeEvent; +import org.bukkit.event.hanging.HangingBreakByEntityEvent; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.player.PlayerAttemptPickupItemEvent; +import org.bukkit.event.player.PlayerDropItemEvent; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.weather.WeatherChangeEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.java.JavaPlugin; +import zone.themcgamer.arcade.Arcade; +import zone.themcgamer.arcade.game.GameState; +import zone.themcgamer.arcade.map.menu.MapVotingMenu; +import zone.themcgamer.arcade.map.menu.TimeVoteMenu; +import zone.themcgamer.arcade.team.menu.SelectTeamMenu; +import zone.themcgamer.core.account.Account; +import zone.themcgamer.core.account.AccountManager; +import zone.themcgamer.core.common.ItemBuilder; +import zone.themcgamer.core.common.SkullTexture; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.core.module.Module; +import zone.themcgamer.core.module.ModuleInfo; +import zone.themcgamer.core.traveller.ServerTraveller; +import zone.themcgamer.data.Rank; + +import java.util.Optional; + +/** + * @author Braydon + */ +@ModuleInfo(name = "Lobby Manager") +public class LobbyManager extends Module { + private static final ItemStack INFORMATION = new ItemBuilder(XMaterial.WRITTEN_BOOK) + .setName("§a§lGame §8» §7How to play?") + .addLoreLine("&7Click to get a small documentation about this game").toItemStack(); + private static final ItemStack KITS = new ItemBuilder(XMaterial.FEATHER) + .setName("§a§lKits §8» §7Select Kit") + .addLoreLine("&7Click to select a kit").toItemStack(); + + private static final ItemStack TEAM_SELECTOR = new ItemBuilder(XMaterial.PLAYER_HEAD) + .setSkullOwner(SkullTexture.TEAM_UNDYED) + .setName("§a§lTeams §8» §7Select Team") + .addLoreLine("&7Click to select a team").toItemStack(); + public static final ItemStack MAP_VOTE = new ItemBuilder(XMaterial.BOOKSHELF) + .setName("§a§lVote §8» §7Select Map") + .toItemStack(); + private static final ItemStack TIME_VOTE = new ItemBuilder(XMaterial.CLOCK) + .setName("§a§lVote §8» §7Select Time") + .addLoreLine("&7Click to select a time").toItemStack(); + + private static final ItemStack GO_BACK_LOBBY = new ItemBuilder(XMaterial.ORANGE_BED) + .setName("§a§lGame §8» §c§lGo back to lobby") + .addLoreLine("&7Click to go back to a lobby server").toItemStack(); + + private final ArcadeManager arcadeManager; + private final ServerTraveller traveller; + + public LobbyManager(JavaPlugin plugin, ArcadeManager arcadeManager, ServerTraveller traveller) { + super(plugin); + this.arcadeManager = arcadeManager; + this.traveller = traveller; + } + + @EventHandler + private void onJoin(PlayerJoinEvent event) { + Player player = event.getPlayer(); + GameState state = arcadeManager.getState(); + if (state == GameState.PLAYING || state == GameState.ENDING) + return; + player.getInventory().setItem(0, INFORMATION); + player.getInventory().setItem(1, KITS); + player.getInventory().setItem(3, TEAM_SELECTOR); + player.getInventory().setItem(4, MAP_VOTE); + player.getInventory().setItem(5, TIME_VOTE); + player.getInventory().setItem(8, GO_BACK_LOBBY); + player.getInventory().setHeldItemSlot(0); + } + + @EventHandler + private void onInventoryClick(InventoryClickEvent event) { + Player player = (Player) event.getWhoClicked(); + Inventory inventory = event.getClickedInventory(); + GameState state = arcadeManager.getState(); + if (state == GameState.PLAYING || state == GameState.ENDING) + return; + if (inventory == null) + return; + if (player.getInventory().equals(inventory) && player.getGameMode() != GameMode.CREATIVE) + event.setCancelled(true); + } + + @EventHandler + private void onInteract(PlayerInteractEvent event) { + if (event.getAction() == Action.PHYSICAL) + return; + Player player = event.getPlayer(); + ItemStack item = event.getItem(); + if (item == null) + return; + if (item.isSimilar(TEAM_SELECTOR)) + new SelectTeamMenu(player, arcadeManager.getGame()).open(); + else if (item.isSimilar(MAP_VOTE)) { + if (!arcadeManager.getMapVotingManager().isVoting()) { + player.sendMessage(Style.main("Voting", "&cYou can not vote at this moment, waiting for more players...")); + return; + } + new MapVotingMenu(player, arcadeManager.getMapVotingManager()).open(); + } else if (item.isSimilar(TIME_VOTE)) { + Optional optionalAccount = AccountManager.fromCache(player.getUniqueId()); + if (optionalAccount.isPresent() && (!optionalAccount.get().hasRank(Rank.GAMER))) { + player.sendMessage(Style.rankRequired(Rank.GAMER)); + return; + } + new TimeVoteMenu(player).open(); + } else if (item.isSimilar(GO_BACK_LOBBY)) + traveller.sendPlayer(player,"Hub"); + } + + @EventHandler + private void onDamage(EntityDamageEvent event) { + Entity entity = event.getEntity(); + GameState state = arcadeManager.getState(); + if (state == GameState.PLAYING || state == GameState.ENDING) + return; + if (event.getCause() == EntityDamageEvent.DamageCause.VOID && entity instanceof Player) + entity.teleport(Arcade.INSTANCE.getSpawn()); + event.setCancelled(true); + } + + @EventHandler + private void onPickupItem(PlayerAttemptPickupItemEvent event) { + GameState state = arcadeManager.getState(); + if (state == GameState.PLAYING || state == GameState.ENDING) + return; + if (event.getPlayer().getGameMode() != GameMode.CREATIVE) + event.setCancelled(true); + } + + @EventHandler + private void onDropItem(PlayerDropItemEvent event) { + GameState state = arcadeManager.getState(); + if (state == GameState.PLAYING || state == GameState.ENDING) + return; + if (event.getPlayer().getGameMode() != GameMode.CREATIVE) + event.setCancelled(true); + } + + @EventHandler + private void onFoodLevelChange(FoodLevelChangeEvent event) { + GameState state = arcadeManager.getState(); + if (state == GameState.PLAYING || state == GameState.ENDING) + return; + event.setCancelled(true); + } + + @EventHandler + private void onBlockPlace(BlockPlaceEvent event) { + GameState state = arcadeManager.getState(); + if (state == GameState.PLAYING || state == GameState.ENDING) + return; + if (event.getPlayer().getGameMode() == GameMode.CREATIVE) + return; + event.setCancelled(true); + } + + @EventHandler + private void onBlockBreak(BlockBreakEvent event) { + GameState state = arcadeManager.getState(); + if (state == GameState.PLAYING || state == GameState.ENDING) + return; + if (event.getPlayer().getGameMode() == GameMode.CREATIVE) + return; + event.setCancelled(true); + } + + @EventHandler + private void onWeatherChange(WeatherChangeEvent event) { + GameState state = arcadeManager.getState(); + if (state == GameState.PLAYING || state == GameState.ENDING) + return; + event.setCancelled(true); + } + + @EventHandler + private void onTnTPrime(ExplosionPrimeEvent event) { + GameState state = arcadeManager.getState(); + if (state == GameState.PLAYING || state == GameState.ENDING) + return; + event.setCancelled(true); + } + + @EventHandler + private void onLeaveDecay(LeavesDecayEvent event) { + GameState state = arcadeManager.getState(); + if (state == GameState.PLAYING || state == GameState.ENDING) + return; + event.setCancelled(true); + } + + @EventHandler + private void entityChangeSoil(PlayerInteractEvent event) { + GameState state = arcadeManager.getState(); + if (state == GameState.PLAYING || state == GameState.ENDING) + return; + if (event.getAction() != Action.PHYSICAL) + return; + if (event.getClickedBlock().getType() == XMaterial.FARMLAND.parseMaterial()) + event.setCancelled(true); + } + + @EventHandler + public void onHangingBreakByEntity(HangingBreakByEntityEvent event) { + GameState state = arcadeManager.getState(); + if (state == GameState.PLAYING || state == GameState.ENDING) + return; + if (event.getRemover() instanceof Player) + event.setCancelled(true); + } +} \ No newline at end of file diff --git a/arcade/src/main/java/zone/themcgamer/arcade/map/GameMap.java b/arcade/src/main/java/zone/themcgamer/arcade/map/GameMap.java new file mode 100644 index 0000000..0badae3 --- /dev/null +++ b/arcade/src/main/java/zone/themcgamer/arcade/map/GameMap.java @@ -0,0 +1,15 @@ +package zone.themcgamer.arcade.map; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.bukkit.World; +import zone.themcgamer.core.world.MGZWorld; + +/** + * @author Braydon + */ +@AllArgsConstructor @Getter +public class GameMap { + private final MGZWorld mgzWorld; + private final World bukkitWorld; +} \ No newline at end of file diff --git a/arcade/src/main/java/zone/themcgamer/arcade/map/MapManager.java b/arcade/src/main/java/zone/themcgamer/arcade/map/MapManager.java new file mode 100644 index 0000000..bc251d0 --- /dev/null +++ b/arcade/src/main/java/zone/themcgamer/arcade/map/MapManager.java @@ -0,0 +1,64 @@ +package zone.themcgamer.arcade.map; + +import lombok.Getter; +import org.bukkit.plugin.java.JavaPlugin; +import zone.themcgamer.common.ZipUtils; +import zone.themcgamer.core.game.MGZGame; +import zone.themcgamer.core.module.Module; +import zone.themcgamer.core.module.ModuleInfo; +import zone.themcgamer.core.world.MGZWorld; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; + +/** + * @author Braydon + */ +@ModuleInfo(name = "Map Manager") @Getter +public class MapManager extends Module { + private final List maps = new ArrayList<>(); + + public MapManager(JavaPlugin plugin) { + super(plugin); + } + + public void withConsumer(Consumer> consumer) { + consumer.accept(CompletableFuture.runAsync(() -> { + File mapsDirectory = new File("maps"); + if (!mapsDirectory.exists()) + mapsDirectory.mkdirs(); + for (MGZGame game : MGZGame.values()) { + File parsedMapsDirectory = new File(File.separator + "home" + File.separator + "minecraft" + File.separator + + "upload" + File.separator + "maps" + File.separator + game.name()); + if (!parsedMapsDirectory.exists()) + continue; + File[] files = parsedMapsDirectory.listFiles(); + if (files == null) + continue; + for (File file : files) { + String fileName = file.getName(); + String[] split = fileName.split("\\."); + if (split.length < 1) + continue; + String lastDottedString = split[split.length - 1]; + if (!lastDottedString.equals("zip")) + continue; + File targetDirectory = new File(mapsDirectory, game.name() + File.separator + + file.getName().substring(0, fileName.indexOf(lastDottedString) - 1)); + if (!targetDirectory.exists()) + targetDirectory.mkdirs(); + try { + ZipUtils.unzip(file, targetDirectory); + maps.add(new MGZWorld(targetDirectory)); + } catch (IOException ex) { + ex.printStackTrace(); + } + } + } + })); + } +} \ No newline at end of file diff --git a/arcade/src/main/java/zone/themcgamer/arcade/map/MapVotingManager.java b/arcade/src/main/java/zone/themcgamer/arcade/map/MapVotingManager.java new file mode 100644 index 0000000..715d95c --- /dev/null +++ b/arcade/src/main/java/zone/themcgamer/arcade/map/MapVotingManager.java @@ -0,0 +1,115 @@ +package zone.themcgamer.arcade.map; + +import com.cryptomorin.xseries.XSound; +import lombok.Getter; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.plugin.java.JavaPlugin; +import zone.themcgamer.arcade.game.GameState; +import zone.themcgamer.arcade.manager.ArcadeManager; +import zone.themcgamer.arcade.manager.LobbyManager; +import zone.themcgamer.arcade.map.event.MapVoteWinEvent; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.core.common.scheduler.ScheduleType; +import zone.themcgamer.core.common.scheduler.event.SchedulerEvent; +import zone.themcgamer.core.module.Module; +import zone.themcgamer.core.module.ModuleInfo; +import zone.themcgamer.core.world.MGZWorld; +import zone.themcgamer.core.world.WorldCategory; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @author Braydon + */ +@ModuleInfo(name = "Map Voting") +public class MapVotingManager extends Module { + private final ArcadeManager arcadeManager; + private final MapManager mapManager; + + @Getter private boolean voting; + private long startedVoting, votingTime; + @Getter private final Map maps = new HashMap<>(); + @Getter private final Set voted = new HashSet<>(); + + public MapVotingManager(JavaPlugin plugin, ArcadeManager arcadeManager, MapManager mapManager) { + super(plugin); + this.arcadeManager = arcadeManager; + this.mapManager = mapManager; + } + + @EventHandler + private void onSchedule(SchedulerEvent event) { + if (event.getType() != ScheduleType.SECOND + || !voting + || startedVoting == -1L + || (System.currentTimeMillis() - startedVoting) < votingTime) + return; + List> entries = new ArrayList<>(maps.entrySet()); + entries.sort((a, b) -> Integer.compare(b.getValue(), a.getValue())); + Map.Entry entry = entries.get(0); + MGZWorld map = entry.getKey(); + int votes = entry.getValue(); + Bukkit.getPluginManager().callEvent(new MapVoteWinEvent(map, votes)); + + Bukkit.broadcastMessage(Style.main("Voting", "Map §f" + map.getName() + " §7won with §6" + + votes + " §7vote" + (votes == 1 ? "" : "s") + "!")); + stopVoting(); + } + + @EventHandler + private void onJoin(PlayerJoinEvent event) { + if (voting || arcadeManager.getState() != GameState.LOBBY) + return; + if (Bukkit.getOnlinePlayers().size() >= arcadeManager.getGame().getMgzGame().getMinPlayers()) { + startVoting(); + } + } + + @EventHandler + private void onQuit(PlayerQuitEvent event) { + if (Bukkit.getOnlinePlayers().size() - 1 < arcadeManager.getGame().getMgzGame().getMinPlayers() && voting) { + stopVoting(); + Bukkit.broadcastMessage(Style.main("Voting", "§cMap voting has ended as there are not enough players!")); + } + } + + public void startVoting() { + startVoting(TimeUnit.SECONDS.toMillis(30L)); + } + + public void startVoting(long time) { + List worldCategories = Arrays.asList(arcadeManager.getGame().getMgzGame().getWorldCategories()); + List maps = mapManager.getMaps().stream() + .filter(mgzWorld -> worldCategories.contains(mgzWorld.getCategory())) + .collect(Collectors.toList()); + if (maps.isEmpty()) + return; + Collections.shuffle(maps); + voting = true; + startedVoting = System.currentTimeMillis(); + votingTime = time; + for (int i = 0; i < Math.min(maps.size(), 5); i++) + this.maps.put(maps.get(i), 0); + if (!voting) + return; + for (Player player : Bukkit.getOnlinePlayers()) { + player.playSound(player.getEyeLocation(), XSound.ENTITY_VILLAGER_AMBIENT.parseSound(), 0.9F, 0.1F); + player.sendMessage(Style.main("Voting", "Now let's vote for a map you like, &6" + player.getName())); + } + } + + public void stopVoting() { + voting = false; + startedVoting = votingTime = -1L; + maps.clear(); + voted.clear(); + for (Player player : Bukkit.getOnlinePlayers()) + player.getInventory().remove(LobbyManager.MAP_VOTE); + } +} \ No newline at end of file diff --git a/arcade/src/main/java/zone/themcgamer/arcade/map/event/MapTimeVoteWinEvent.java b/arcade/src/main/java/zone/themcgamer/arcade/map/event/MapTimeVoteWinEvent.java new file mode 100644 index 0000000..b0b241b --- /dev/null +++ b/arcade/src/main/java/zone/themcgamer/arcade/map/event/MapTimeVoteWinEvent.java @@ -0,0 +1,15 @@ +package zone.themcgamer.arcade.map.event; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import zone.themcgamer.core.common.WorldTime; +import zone.themcgamer.core.common.WrappedBukkitEvent; + +/** + * @author Braydon + */ +@AllArgsConstructor @Getter +public class MapTimeVoteWinEvent extends WrappedBukkitEvent { + private final WorldTime worldTime; + private final int votes; +} \ No newline at end of file diff --git a/arcade/src/main/java/zone/themcgamer/arcade/map/event/MapVoteWinEvent.java b/arcade/src/main/java/zone/themcgamer/arcade/map/event/MapVoteWinEvent.java new file mode 100644 index 0000000..d7d104c --- /dev/null +++ b/arcade/src/main/java/zone/themcgamer/arcade/map/event/MapVoteWinEvent.java @@ -0,0 +1,15 @@ +package zone.themcgamer.arcade.map.event; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import zone.themcgamer.core.common.WrappedBukkitEvent; +import zone.themcgamer.core.world.MGZWorld; + +/** + * @author Braydon + */ +@AllArgsConstructor @Getter +public class MapVoteWinEvent extends WrappedBukkitEvent { + private final MGZWorld map; + private final int votes; +} \ No newline at end of file diff --git a/arcade/src/main/java/zone/themcgamer/arcade/map/menu/MapVotingMenu.java b/arcade/src/main/java/zone/themcgamer/arcade/map/menu/MapVotingMenu.java new file mode 100644 index 0000000..f1c1d01 --- /dev/null +++ b/arcade/src/main/java/zone/themcgamer/arcade/map/menu/MapVotingMenu.java @@ -0,0 +1,70 @@ +package zone.themcgamer.arcade.map.menu; + +import com.cryptomorin.xseries.XMaterial; +import com.cryptomorin.xseries.XSound; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.Nullable; +import zone.themcgamer.arcade.map.MapVotingManager; +import zone.themcgamer.common.RandomUtils; +import zone.themcgamer.core.common.ItemBuilder; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.core.common.menu.Button; +import zone.themcgamer.core.common.menu.Menu; +import zone.themcgamer.core.common.menu.MenuType; +import zone.themcgamer.core.world.MGZWorld; + +import java.util.ArrayList; +import java.util.Map; + +/** + * @author Braydon + */ +public class MapVotingMenu extends Menu { + private final MapVotingManager mapVotingManager; + + public MapVotingMenu(Player player, MapVotingManager mapVotingManager) { + super(player, "Map Voting", 3, MenuType.CHEST); + this.mapVotingManager = mapVotingManager; + } + + @Override + protected void onOpen() { + set(1, 1, new Button(new ItemBuilder(XMaterial.BOOKSHELF) + .setName("§6§lRandom") + .setLore( + "", + "§7Click to vote for a random map" + ).toItemStack(), event -> vote(null))); + + int slot = 3; + for (Map.Entry entry : mapVotingManager.getMaps().entrySet()) { + MGZWorld map = entry.getKey(); + set(1, slot++, new Button(new ItemBuilder(XMaterial.PAPER) + .setName("§6§l" + map.getName()) + .setLore( + "", + "§7Votes §f" + entry.getValue(), + "§7Made By §f" + map.getAuthor() + ).toItemStack(), event -> vote(map))); + } + } + + private void vote(@Nullable MGZWorld map) { + close(); + if (mapVotingManager.getVoted().contains(player.getUniqueId())) { + player.sendMessage(Style.error("Voting", "§cYou already voted!")); + player.playSound(player.getLocation(), XSound.BLOCK_NOTE_BLOCK_BASS.parseSound(), 10, 2); + } else { + if (map == null) + map = RandomUtils.random(new ArrayList<>(mapVotingManager.getMaps().keySet())); + if (map == null) + player.sendMessage(Style.error("Voting", "§cInvalid map!")); + else { + mapVotingManager.getVoted().add(player.getUniqueId()); + mapVotingManager.getMaps().put(map, mapVotingManager.getMaps().getOrDefault(map, 0) + 1); + player.playSound(player.getLocation(), XSound.UI_BUTTON_CLICK.parseSound(),10, 2); + player.sendMessage(Style.main("Voting", "You voted for §f" + map.getName())); + } + } + } +} \ No newline at end of file diff --git a/arcade/src/main/java/zone/themcgamer/arcade/map/menu/TimeVoteMenu.java b/arcade/src/main/java/zone/themcgamer/arcade/map/menu/TimeVoteMenu.java new file mode 100644 index 0000000..a536b51 --- /dev/null +++ b/arcade/src/main/java/zone/themcgamer/arcade/map/menu/TimeVoteMenu.java @@ -0,0 +1,45 @@ +package zone.themcgamer.arcade.map.menu; + +import com.cryptomorin.xseries.XMaterial; +import org.bukkit.entity.Player; +import zone.themcgamer.core.common.ItemBuilder; +import zone.themcgamer.core.common.WorldTime; +import zone.themcgamer.core.common.menu.Button; +import zone.themcgamer.core.common.menu.MenuPattern; +import zone.themcgamer.core.common.menu.MenuType; +import zone.themcgamer.core.common.menu.UpdatableMenu; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class TimeVoteMenu extends UpdatableMenu { + public TimeVoteMenu(Player player) { + super(player, "Time Vote", 3, MenuType.CHEST); + } + + @Override + public void onUpdate() { + fill(new Button(new ItemBuilder(XMaterial.BLACK_STAINED_GLASS_PANE).setName("&7").toItemStack())); + List slots = MenuPattern.getSlots( + "XXXXXXXXX", + "XOXOXOXOX", + "XXXXXXXXX" + ); + int index = 0; + for (WorldTime time : Arrays.asList(WorldTime.SUNRISE, WorldTime.DAY, WorldTime.SUNSET, WorldTime.MIDNIGHT)) { + List lore = new ArrayList<>(); + for (String descriptionLine : time.getDescription()) + lore.add("§7" + descriptionLine); + lore.add(""); + lore.add("§e▪ &7Current votes: &b0"); + lore.add(""); + lore.add("&aClick to vote!"); + ItemBuilder icon = new ItemBuilder(XMaterial.CLOCK).setName("&e&l" + time.getDisplayName()); + icon.setLore(lore); + set(slots.get(index++), new Button(icon.toItemStack(), event -> { + //TODO do the vote shit here + })); + } + } +} \ No newline at end of file diff --git a/arcade/src/main/java/zone/themcgamer/arcade/player/GamePlayer.java b/arcade/src/main/java/zone/themcgamer/arcade/player/GamePlayer.java new file mode 100644 index 0000000..5fc3285 --- /dev/null +++ b/arcade/src/main/java/zone/themcgamer/arcade/player/GamePlayer.java @@ -0,0 +1,41 @@ +package zone.themcgamer.arcade.player; + +import lombok.Getter; +import lombok.Setter; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import zone.themcgamer.arcade.team.Team; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * @author Braydon + */ +@Setter @Getter +public class GamePlayer { + @Getter private static final Map cache = new HashMap<>(); + + private final UUID uuid; + private Team team; + private boolean spectating; + private long logoutTime; + + public GamePlayer(UUID uuid) { + this.uuid = uuid; + cache.put(uuid, this); + } + + public Player getBukkitPlayer() { + return Bukkit.getPlayer(uuid); + } + + public void remove() { + cache.remove(uuid); + } + + public static GamePlayer getPlayer(UUID uuid) { + return cache.get(uuid); + } +} \ No newline at end of file diff --git a/arcade/src/main/java/zone/themcgamer/arcade/player/PlayerDataManager.java b/arcade/src/main/java/zone/themcgamer/arcade/player/PlayerDataManager.java new file mode 100644 index 0000000..7df457e --- /dev/null +++ b/arcade/src/main/java/zone/themcgamer/arcade/player/PlayerDataManager.java @@ -0,0 +1,54 @@ +package zone.themcgamer.arcade.player; + +import org.bukkit.event.EventHandler; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.plugin.java.JavaPlugin; +import zone.themcgamer.core.common.scheduler.ScheduleType; +import zone.themcgamer.core.common.scheduler.event.SchedulerEvent; +import zone.themcgamer.core.module.Module; +import zone.themcgamer.core.module.ModuleInfo; + +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +/** + * @author Braydon + */ +@ModuleInfo(name = "Player Data Manager") +public class PlayerDataManager extends Module { + // The amount of time a player has to rejoin the game after being disconnected. + // If the player does not join within the given time, their stats for this current + // game will be cleared + private static final long MAX_REJOIN_TIME = TimeUnit.MINUTES.toMillis(3L); + + public PlayerDataManager(JavaPlugin plugin) { + super(plugin); + } + + @EventHandler + private void expirePlayers(SchedulerEvent event) { + if (event.getType() != ScheduleType.SECOND) + return; + GamePlayer.getCache().entrySet().removeIf(entry -> { + GamePlayer gamePlayer = entry.getValue(); + if (gamePlayer.getBukkitPlayer() != null) + return false; + return (System.currentTimeMillis() - gamePlayer.getLogoutTime()) >= MAX_REJOIN_TIME; + }); + } + + @EventHandler + private void onJoin(PlayerJoinEvent event) { + UUID uuid = event.getPlayer().getUniqueId(); + if (GamePlayer.getPlayer(uuid) == null) + new GamePlayer(uuid); + } + + @EventHandler + private void onQuit(PlayerQuitEvent event) { + GamePlayer gamePlayer = GamePlayer.getPlayer(event.getPlayer().getUniqueId()); + if (gamePlayer != null) + gamePlayer.setLogoutTime(System.currentTimeMillis()); + } +} \ No newline at end of file diff --git a/arcade/src/main/java/zone/themcgamer/arcade/scoreboard/ArcadeScoreboard.java b/arcade/src/main/java/zone/themcgamer/arcade/scoreboard/ArcadeScoreboard.java new file mode 100644 index 0000000..e3a8917 --- /dev/null +++ b/arcade/src/main/java/zone/themcgamer/arcade/scoreboard/ArcadeScoreboard.java @@ -0,0 +1,88 @@ +package zone.themcgamer.arcade.scoreboard; + +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.entity.Player; +import zone.themcgamer.arcade.Arcade; +import zone.themcgamer.arcade.game.GameState; +import zone.themcgamer.arcade.manager.ArcadeManager; +import zone.themcgamer.arcade.map.GameMap; +import zone.themcgamer.arcade.player.GamePlayer; +import zone.themcgamer.common.DoubleUtils; +import zone.themcgamer.core.account.Account; +import zone.themcgamer.core.account.AccountManager; +import zone.themcgamer.core.animation.impl.WaveAnimation; +import zone.themcgamer.core.common.scoreboard.WritableScoreboard; +import zone.themcgamer.core.plugin.MGZPlugin; +import zone.themcgamer.data.jedis.data.server.MinecraftServer; + +import java.util.Optional; + +/** + * @author Braydon + */ +public class ArcadeScoreboard extends WritableScoreboard { + private final GamePlayer gamePlayer; + private WaveAnimation title; + + public ArcadeScoreboard(Player player) { + super(player); + gamePlayer = GamePlayer.getPlayer(player.getUniqueId()); + } + + @Override + public String getTitle() { + ArcadeManager arcadeManager = Arcade.INSTANCE.getArcadeManager(); + String title = arcadeManager.getGame().getMgzGame().getName(); + if (this.title == null || (!this.title.getInput().equals(title))) { + this.title = new WaveAnimation(title) + .withPrimary(ChatColor.GOLD.toString()) + .withSecondary(ChatColor.WHITE.toString()) + .withBold(); + } + return this.title.next(); + + } + + @Override + public void writeLines() { + Optional optionalAccount = AccountManager.fromCache(player.getUniqueId()); + if (!optionalAccount.isPresent()) { + writeBlank(); + return; + } + Account account = optionalAccount.get(); + ArcadeManager arcadeManager = Arcade.INSTANCE.getArcadeManager(); + GameState state = arcadeManager.getState(); + MinecraftServer minecraftServer = MGZPlugin.getMinecraftServer(); + + GameMap map = arcadeManager.getMap(); + + writeBlank(); + switch (state) { + case LOBBY: + case STARTING: { + write("§e▪ §fKit: §bWarrior"); + write("§e▪ §fGold: §6" + DoubleUtils.format(account.getGold(), true) + " \u26C3"); + writeBlank(); + write("§e▪ §fMap: §b" + (arcadeManager.getMapVotingManager().isVoting() ? "Voting..." : (map == null ? "None" : map.getMgzWorld().getName()))); + write("§e▪ §fState: §b" + state.getDisplayName()); + write("§e▪ §fPlayers: §a" + Bukkit.getOnlinePlayers().size() + "§7/§c" + arcadeManager.getGame().getMgzGame().getMaxPlayers()); + writeBlank(); + write("§e▪ §fGame: §b" + minecraftServer.getId().replace(minecraftServer.getGroup().getName().toLowerCase(), "")); + break; + } + case PLAYING: + case ENDING: { + for (String line : arcadeManager.getGame().getScoreboard(gamePlayer, player)) { + if (line.trim().isEmpty()) + writeBlank(); + else write(line); + } + break; + } + } + writeBlank(); + write("§6themcgamer.zone"); + } +} \ No newline at end of file diff --git a/arcade/src/main/java/zone/themcgamer/arcade/team/Team.java b/arcade/src/main/java/zone/themcgamer/arcade/team/Team.java new file mode 100644 index 0000000..af244e2 --- /dev/null +++ b/arcade/src/main/java/zone/themcgamer/arcade/team/Team.java @@ -0,0 +1,59 @@ +package zone.themcgamer.arcade.team; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.bukkit.ChatColor; +import zone.themcgamer.core.common.SkullTexture; + +/** + * @author Braydon + */ +@AllArgsConstructor @Getter +public class Team { + private final String name; + private final ChatColor color; + private final boolean friendlyFire; + + public String getSkullTexture() { + switch (color) { + case DARK_GREEN: { + return SkullTexture.TEAM_GREEN; + } + case DARK_AQUA: { + return SkullTexture.TEAM_CYAN; + } + case DARK_RED: + case RED: { + return SkullTexture.TEAM_RED; + } + case DARK_PURPLE: { + return SkullTexture.TEAM_PURPLE; + } + case GOLD: { + return SkullTexture.TEAM_ORANGE; + } + case GRAY: { + return SkullTexture.TEAM_LIGHT_GRAY; + } + case DARK_GRAY: { + return SkullTexture.TEAM_GRAY; + } + case BLUE: { + return SkullTexture.TEAM_BLUE; + } + case GREEN: { + return SkullTexture.TEAM_LIME; + } + case AQUA: { + return SkullTexture.TEAM_LIGHT_BLUE; + } + case LIGHT_PURPLE: { + return SkullTexture.TEAM_MAGENTA; + } + case YELLOW: { + return SkullTexture.TEAM_YELLOW; + } + } + return SkullTexture.TEAM_WHITE; + } +} \ No newline at end of file diff --git a/arcade/src/main/java/zone/themcgamer/arcade/team/menu/SelectTeamMenu.java b/arcade/src/main/java/zone/themcgamer/arcade/team/menu/SelectTeamMenu.java new file mode 100644 index 0000000..f937da2 --- /dev/null +++ b/arcade/src/main/java/zone/themcgamer/arcade/team/menu/SelectTeamMenu.java @@ -0,0 +1,76 @@ +package zone.themcgamer.arcade.team.menu; + +import com.cryptomorin.xseries.XMaterial; +import org.bukkit.entity.Player; +import zone.themcgamer.arcade.game.Game; +import zone.themcgamer.arcade.player.GamePlayer; +import zone.themcgamer.arcade.team.Team; +import zone.themcgamer.core.common.ItemBuilder; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.core.common.menu.Button; +import zone.themcgamer.core.common.menu.MenuPattern; +import zone.themcgamer.core.common.menu.MenuType; +import zone.themcgamer.core.common.menu.UpdatableMenu; + +import java.util.ArrayList; +import java.util.List; + +public class SelectTeamMenu extends UpdatableMenu { + private final GamePlayer gamePlayer; + private final Game game; + + public SelectTeamMenu(Player player, Game game) { + super(player, null, 3, MenuType.CHEST); + this.game = game; + gamePlayer = GamePlayer.getPlayer(player.getUniqueId()); + if (gamePlayer == null) + return; + setTitle((gamePlayer.getTeam() == null ? "Pick a Team" : "Current ▪ " + gamePlayer.getTeam().getColor() + gamePlayer.getTeam().getName())); + } + + @Override + public void onUpdate() { + if (gamePlayer == null) + return; + fillBorders(new Button(new ItemBuilder(XMaterial.BLACK_STAINED_GLASS_PANE) + .setName("&7").toItemStack())); + List slots = MenuPattern.getSlots( + "XXXXXXXXX", + "XOOOOOOOX", + "XXXXXXXXX" + ); + int index = 0; + for (Team team : game.getTeams()) { + List lore = new ArrayList<>(); + lore.add(""); + lore.add("§e▪ &7Players: " + team.getColor() + "0"); + lore.add(""); + if (isInTeam(gamePlayer, team)) + lore.add("§aSelected!"); + else lore.add("&7Click to join &f" + team.getColor() + team.getName() + " &7team!"); + set(slots.get(index++), new Button(new ItemBuilder(XMaterial.PLAYER_HEAD) + .setGlow(isInTeam(gamePlayer, team)) + .setSkullOwner(team.getSkullTexture()) + .setName(team.getColor() + team.getName()) + .setLore(lore).toItemStack(), event -> { + if (isInTeam(gamePlayer, team)) { + player.sendMessage(Style.main("Teams", "You're already on this team!")); + return; + } + close(); + //TODO Check if team is unbalanced + + gamePlayer.setTeam(team); + player.getInventory().setItem(3, new ItemBuilder(XMaterial.PLAYER_HEAD) + .setSkullOwner(team.getSkullTexture()) + .setName("§a§lTeams §8» §7Select team") + .addLoreLine("&7Click to select a team").toItemStack()); + player.sendMessage(Style.main("Teams","You've joined the §f" + team.getColor() + team.getName() + " &7team!")); + })); + } + } + + protected boolean isInTeam(GamePlayer gamePlayer, Team team) { + return (gamePlayer.getTeam() != null && (gamePlayer.getTeam().equals(team))); + } +} diff --git a/arcade/src/main/resources/plugin.yml b/arcade/src/main/resources/plugin.yml new file mode 100644 index 0000000..9795dc4 --- /dev/null +++ b/arcade/src/main/resources/plugin.yml @@ -0,0 +1,5 @@ +name: Arcade +version: 1.0-SNAPSHOT +api-version: 1.13 +main: zone.themcgamer.arcade.Arcade +author: MGZ Development Team \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..c6b8271 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,137 @@ +import java.util.Optional as javaOptional + +plugins { + `java-library` + `maven-publish` + kotlin("jvm") version "1.4.30-RC" + id("com.github.johnrengelman.shadow") version "6.1.0" + id("com.gorylenko.gradle-git-properties") version "2.2.2" +} + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +allprojects { + group = "zone.themcgamer" + version = "1.0-SNAPSHOT" +} + +subprojects { + + apply { + plugin("java-library") + plugin("kotlin") + plugin("com.github.johnrengelman.shadow") + plugin("com.gorylenko.gradle-git-properties") + plugin("maven-publish") + } + + dependencies { + compileOnly(kotlin("stdlib-jdk8")) + compileOnly("org.jetbrains:annotations:20.1.0") + + // lombok + compileOnly("org.projectlombok:lombok:1.18.16") + annotationProcessor("org.projectlombok:lombok:1.18.16") + + testCompileOnly("org.projectlombok:lombok:1.18.16") + testAnnotationProcessor("org.projectlombok:lombok:1.18.16") + + compileOnly("org.slf4j:slf4j-simple:1.7.30") + + implementation("com.google.code.gson:gson:2.7") + + implementation("commons-io:commons-io:2.6") + + implementation("com.squareup.okhttp3:okhttp:4.10.0-RC1") + } + + gitProperties { + customProperty("insane_module", name) + } + + tasks { + compileJava { + options.compilerArgs.add("-parameters") + options.forkOptions.executable = "javac" + options.encoding = "UTF-8" + } + + compileKotlin { + kotlinOptions.jvmTarget = "11"; + } + } + + publishing { + publications { + create("maven") { + from(components["java"]) + } + } + + repositories { + maven { + name = project.name + url = uri("https://mvn.cnetwork.club/repository/${project.name}/") + + credentials { + username = System.getenv("NEXUS_USERNAME") + password = System.getenv("NEXUS_PASSWORD") + } + } + } + + getPropertySafe("vcsImcPrivateToken").ifPresent { token -> + repositories { + maven { + url = uri("https://vcs.cnetwork.club/api/v4/projects/1/packages/maven") + credentials(HttpHeaderCredentials::class) { + name = "Private-Token" + value = token // the variable resides in ~/.gradle/gradle.properties + } + authentication { + create("header") + } + } + } + } + } + + repositories { + mavenLocal() + mavenCentral() + jcenter() + + maven { + url = uri("https://mvn.cnetwork.club/repository/public/") + + credentials { + username = getEnv("NEXUS_USERNAME").orElseGet { + getPropertySafe("mavenUsername") + .orElseThrow { IllegalArgumentException("Central repo not configured") } + } + password = getEnv("NEXUS_PASSWORD").orElseGet { + getPropertySafe("mavenPassword") + .orElseThrow { IllegalArgumentException("Central repo not configured") } + } + } + + } + } +} + +repositories { + mavenCentral() +} + +fun getEnv(env: String): java.util.Optional { + return javaOptional.ofNullable(System.getenv(env)) +} + +fun getPropertySafe(property: String): javaOptional { + return if (project.hasProperty(property)) javaOptional.of( + project.property(property).toString() + ) else javaOptional.empty() +} diff --git a/buildserver/build.gradle.kts b/buildserver/build.gradle.kts new file mode 100644 index 0000000..9ccdef2 --- /dev/null +++ b/buildserver/build.gradle.kts @@ -0,0 +1,19 @@ +dependencies { + implementation(project(":core")) + compileOnly("com.destroystokyo:paperspigot:1.12.2") + implementation("com.github.cryptomorin:XSeries:7.8.0") +} + +tasks { + processResources { + val tokens = mapOf("version" to project.version) + from(sourceSets["main"].resources.srcDirs) { + filter("tokens" to tokens) + } + } + + shadowJar { + archiveFileName.set("${project.rootProject.name}-${project.name}-v${project.version}.jar") + destinationDir = file("$rootDir/output") + } +} \ No newline at end of file diff --git a/buildserver/src/main/java/zone/themcgamer/buildServer/Build.java b/buildserver/src/main/java/zone/themcgamer/buildServer/Build.java new file mode 100644 index 0000000..adf64be --- /dev/null +++ b/buildserver/src/main/java/zone/themcgamer/buildServer/Build.java @@ -0,0 +1,41 @@ +package zone.themcgamer.buildServer; + +import lombok.Getter; +import org.bukkit.Bukkit; +import org.bukkit.World; +import zone.themcgamer.buildServer.listener.PlayerListener; +import zone.themcgamer.buildServer.world.WorldManager; +import zone.themcgamer.core.chat.ChatManager; +import zone.themcgamer.core.chat.component.IChatComponent; +import zone.themcgamer.core.chat.component.impl.BasicNameComponent; +import zone.themcgamer.core.chat.component.impl.BasicRankComponent; +import zone.themcgamer.core.plugin.MGZPlugin; +import zone.themcgamer.core.plugin.Startup; + +/** + * @author Braydon + */ +@Getter +public class Build extends MGZPlugin { + public static Build INSTANCE; + + private World mainWorld; + + @Override + public void onEnable() { + super.onEnable(); + INSTANCE = this; + } + + @Startup + public void loadBuildServer() { + mainWorld = Bukkit.getWorlds().get(0); + WorldManager worldManager = new WorldManager(this); + new PlayerListener(this, worldManager); + + new ChatManager(this, badSportSystem, new IChatComponent[] { + new BasicRankComponent(), + new BasicNameComponent() + }); + } +} \ No newline at end of file diff --git a/buildserver/src/main/java/zone/themcgamer/buildServer/backup/BackupTask.java b/buildserver/src/main/java/zone/themcgamer/buildServer/backup/BackupTask.java new file mode 100644 index 0000000..df2196b --- /dev/null +++ b/buildserver/src/main/java/zone/themcgamer/buildServer/backup/BackupTask.java @@ -0,0 +1,45 @@ +package zone.themcgamer.buildServer.backup; + +import org.apache.commons.io.FileUtils; +import org.bukkit.Bukkit; +import org.bukkit.plugin.java.JavaPlugin; +import zone.themcgamer.common.ZipUtils; +import zone.themcgamer.core.world.MGZWorld; + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * @author Braydon + */ +public class BackupTask { + private static final int MAX_BACKUP_SIZE = 15; + + public BackupTask(JavaPlugin plugin, MGZWorld world) { + if (world.getWorld() != null) + throw new IllegalStateException("Cannot backup a loaded world"); + File backupDirectory = new File("backups" + File.separator + world.getCategory().name() + File.separator + world.getName()); + if (!backupDirectory.exists()) + backupDirectory.mkdirs(); + plugin.getLogger().info("Backing up world \"" + world.getName() + "\" under category \"" + world.getCategory().name() + "\""); + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + File[] files = backupDirectory.listFiles(); + if (files != null && (files.length > MAX_BACKUP_SIZE)) { + File firstBackup = null; + for (File file : files) { + if (firstBackup == null || (file.lastModified() < firstBackup.lastModified())) + firstBackup = file; + } + if (firstBackup != null) + FileUtils.deleteQuietly(firstBackup); + } + File worldDirectory = new File("maps" + File.separator + world.getCategory().name() + File.separator + world.getName()); + if (!worldDirectory.exists()) + return; + File zipFile = new File(backupDirectory, new SimpleDateFormat("yyyy-MM-dd HH-mm-ss").format(new Date()) + ".zip"); + ZipUtils.zip(worldDirectory.getPath(), zipFile.getPath()); + plugin.getLogger().info("Created backup \"" + zipFile.getPath() + "\""); + }); + } +} \ No newline at end of file diff --git a/buildserver/src/main/java/zone/themcgamer/buildServer/listener/PlayerListener.java b/buildserver/src/main/java/zone/themcgamer/buildServer/listener/PlayerListener.java new file mode 100644 index 0000000..988e536 --- /dev/null +++ b/buildserver/src/main/java/zone/themcgamer/buildServer/listener/PlayerListener.java @@ -0,0 +1,109 @@ +package zone.themcgamer.buildServer.listener; + +import com.google.common.base.Strings; +import org.bukkit.Bukkit; +import org.bukkit.GameMode; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.event.entity.FoodLevelChangeEvent; +import org.bukkit.event.player.AsyncPlayerChatEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerLoginEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.plugin.java.JavaPlugin; +import zone.themcgamer.buildServer.Build; +import zone.themcgamer.buildServer.world.WorldManager; +import zone.themcgamer.core.account.Account; +import zone.themcgamer.core.account.AccountManager; +import zone.themcgamer.core.common.PlayerUtils; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.core.world.MGZWorld; +import zone.themcgamer.data.Rank; + +import java.util.Optional; + +/** + * @author Braydon + */ +public class PlayerListener implements Listener { + private final WorldManager worldManager; + + public PlayerListener(JavaPlugin plugin, WorldManager worldManager) { + this.worldManager = worldManager; + Bukkit.getScheduler().scheduleSyncRepeatingTask(plugin, () -> { + for (Player player : Bukkit.getOnlinePlayers()) { + if (player.getGameMode() != GameMode.CREATIVE) + player.setGameMode(GameMode.CREATIVE); + if (!player.getAllowFlight()) + player.setAllowFlight(true); + } + }, 20L, 20L); + Bukkit.getPluginManager().registerEvents(this, plugin); + } + + @EventHandler(priority = EventPriority.HIGHEST) + private void onLogin(PlayerLoginEvent event) { + if (event.getResult() != PlayerLoginEvent.Result.ALLOWED) + return; + Player player = event.getPlayer(); + Optional optionalAccount = AccountManager.fromCache(player.getUniqueId()); + if (player.isOp() || player.isWhitelisted() || (optionalAccount.isPresent() && (optionalAccount.get().hasRank(Rank.BUILDER)))) + return; + event.disallow(PlayerLoginEvent.Result.KICK_OTHER, "§cOnly builders can join this server!"); + } + + @EventHandler(priority = EventPriority.HIGHEST) + private void onJoin(PlayerJoinEvent event) { + Player player = event.getPlayer(); + PlayerUtils.reset(player, true, false, GameMode.CREATIVE); + + player.sendMessage(Style.color("&8&m" + Strings.repeat("-", 30))); + player.sendMessage(""); + player.sendMessage(Style.color(" &e➢ &6&lBuild")); + player.sendMessage(""); + player.sendMessage(Style.color(" &7For a list of commands, use &f/help")); + player.sendMessage(Style.color(" &7Wanna learn how to use the build system? Use &f/tutorial")); + player.sendMessage(""); + player.sendMessage(Style.color("&8&m" + Strings.repeat("-", 30))); + + player.teleport(Build.INSTANCE.getMainWorld().getSpawnLocation()); + event.setJoinMessage(Style.color("&8[&a+&8] &7" + player.getName())); + } + + @EventHandler + private void onChat(AsyncPlayerChatEvent event) { + Player player = event.getPlayer(); + Optional optionalAccount = AccountManager.fromCache(player.getUniqueId()); + if (!optionalAccount.isPresent()) { + player.sendMessage(Style.error("Chat", "§cCannot send chat message")); + return; + } + MGZWorld mgzWorld = worldManager.getWorld(player.getWorld()); + event.setFormat((mgzWorld == null ? "" : "§7[" + mgzWorld.getName() + "§7] ") + "§f" + + optionalAccount.get().getPrimaryRank().getColor() + player.getName() + "§7: §f" + event.getMessage().replaceAll("%", "%%")); + } + + @EventHandler + private void onDamage(EntityDamageEvent event) { + Entity entity = event.getEntity(); + if (entity instanceof Player) { + event.setCancelled(true); + if (event.getCause() == EntityDamageEvent.DamageCause.VOID) + entity.teleport(entity.getWorld().getSpawnLocation()); + } + } + + @EventHandler + private void onFoodLevelChange(FoodLevelChangeEvent event) { + event.setCancelled(true); + } + + @EventHandler(priority = EventPriority.HIGHEST) + private void onQuit(PlayerQuitEvent event) { + event.setQuitMessage(Style.color("&8[&c-&8] &7" + event.getPlayer().getName())); + } +} \ No newline at end of file diff --git a/buildserver/src/main/java/zone/themcgamer/buildServer/parse/ParseTask.java b/buildserver/src/main/java/zone/themcgamer/buildServer/parse/ParseTask.java new file mode 100644 index 0000000..bee211e --- /dev/null +++ b/buildserver/src/main/java/zone/themcgamer/buildServer/parse/ParseTask.java @@ -0,0 +1,165 @@ +package zone.themcgamer.buildServer.parse; + +import lombok.Getter; +import lombok.Setter; +import org.apache.commons.io.FileUtils; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.block.Sign; +import zone.themcgamer.common.DoubleUtils; +import zone.themcgamer.common.MathUtils; +import zone.themcgamer.common.ZipUtils; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.core.world.MGZWorld; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author Braydon + */ +public class ParseTask { + @Getter private final MGZWorld mgzWorld; + private final int radius; + private final List ids; + + private World world; + private Location center; + private long started; + + @Setter private boolean preparing = true; + private int x, y, z, parsedBlocks; + @Getter private boolean completed; + + private final Map> dataPoints = new HashMap<>(); + + public ParseTask(MGZWorld mgzWorld, int radius, List ids) { + this.mgzWorld = mgzWorld; + this.radius = radius; + this.ids = ids; + x = z = -radius; + } + + public void start(World world, Location center) { + if (!preparing) + return; + this.world = world; + this.center = center; + started = System.currentTimeMillis(); + preparing = false; + } + + public void run() { + if (preparing) + return; + long started = System.currentTimeMillis(); + for (; x <= radius; x++) { + for (; y <= 256; y++) { + for (; z <= radius; z++) { + if ((System.currentTimeMillis() - started) >= 10L) + return; + parsedBlocks++; + if (parsedBlocks % 15_000_000 == 0) { + double complete = (double) parsedBlocks / 1_000_000; + double total = (double) ((radius * 2) * 256 * (radius * 2)) / 1_000_000; + double percent = MathUtils.round(complete / total * 100, 1); + Bukkit.broadcastMessage(Style.main("Map", "Parse of map §b" + mgzWorld.getName() + " §7is §6" + percent + "% §7complete")); + } + Block block = world.getBlockAt(center.getBlockX() + x, y, center.getBlockZ() + z); + Location blockLocation = block.getLocation(); + blockLocation.setX(blockLocation.getBlockX() + .5); + blockLocation.setZ(blockLocation.getBlockZ() + .5); + + if (block.getType() == Material.SPONGE) { + Block blockAbove = block.getRelative(BlockFace.UP); + if (blockAbove.getType() == Material.SIGN || blockAbove.getType() == Material.SIGN_POST) { + Sign sign = (Sign) blockAbove.getState(); + String[] lines = sign.getLines(); + if (lines.length < 1) + continue; + StringBuilder signText = new StringBuilder(lines[0]); + if (lines.length >= 2) + signText.append(" ").append(lines[1]); + if (lines.length >= 3) + signText.append(" ").append(lines[2]); + if (lines.length >= 4) + signText.append(" ").append(lines[3]); + String dataPointName = signText.toString().trim(); + + List locations = dataPoints.getOrDefault(dataPointName, new ArrayList<>()); + locations.add(blockLocation); + dataPoints.put(dataPointName, locations); + + block.setType(Material.AIR); + blockAbove.setType(Material.AIR); + } + } else if (ids.contains(block.getTypeId())) { + String dataPointName = block.getType().name(); + List locations = dataPoints.getOrDefault(dataPointName, new ArrayList<>()); + locations.add(blockLocation); + dataPoints.put(dataPointName, locations); + } + } + z = -radius; + } + y = 0; + } + // Unloading the world + Bukkit.unloadWorld(world, true); + + File directory = world.getWorldFolder(); + + // Removing unnecessary files + File[] files = directory.listFiles(); + if (files != null) { + for (File file : files) { + String fileName = file.getName(); + if (fileName.equals("level.dat") || fileName.equals("region") || fileName.equals(MGZWorld.FILE_NAME)) + continue; + FileUtils.deleteQuietly(file); + } + } + // Saving properties file + mgzWorld.setDataPoints(dataPoints); + mgzWorld.save(new File(world.getWorldFolder(), MGZWorld.FILE_NAME)); + + // Zipping the parsed world + File targetDirectory = new File(File.separator + "home" + File.separator + "minecraft" + File.separator + "upload" + File.separator + + "maps" + File.separator + mgzWorld.getCategory().name()); + if (!targetDirectory.exists()) + targetDirectory.mkdirs(); + File targetFile = new File(targetDirectory, mgzWorld.getName().replaceAll(" ", "_") + ".zip"); + if (targetFile.exists()) + FileUtils.deleteQuietly(targetFile); + ZipUtils.zip(directory.getPath(), targetFile.getPath()); + + // Deleting the parsed world + FileUtils.deleteQuietly(directory); + + // Marking the parse as complete + completed = true; + + // Announcing the parse summary + long elapsed = System.currentTimeMillis() - this.started; + String time; + if (elapsed < 1000) + time = elapsed + "ms"; + else time = (elapsed / 1000) + " seconds"; + + Bukkit.broadcastMessage(Style.main("Map", "Parse summary of §b" + mgzWorld.getName() + "§7:")); + Bukkit.broadcastMessage(Style.color(" §8- §7Blocks §f" + DoubleUtils.format(parsedBlocks, false))); + if (!dataPoints.isEmpty()) { + Bukkit.broadcastMessage(" §bData Points"); + for (Map.Entry> entry : dataPoints.entrySet()) + Bukkit.broadcastMessage(" §6" + entry.getKey() + " §f" + entry.getValue().size()); + } + Bukkit.broadcastMessage(Style.color(" §8- §7Time §f" + time)); + } +} \ No newline at end of file diff --git a/buildserver/src/main/java/zone/themcgamer/buildServer/parse/command/ParseCommand.java b/buildserver/src/main/java/zone/themcgamer/buildServer/parse/command/ParseCommand.java new file mode 100644 index 0000000..d233428 --- /dev/null +++ b/buildserver/src/main/java/zone/themcgamer/buildServer/parse/command/ParseCommand.java @@ -0,0 +1,62 @@ +package zone.themcgamer.buildServer.parse.command; + +import lombok.AllArgsConstructor; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import zone.themcgamer.buildServer.world.WorldManager; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.core.world.MGZWorld; +import zone.themcgamer.data.Rank; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author Braydon + */ +@AllArgsConstructor +public class ParseCommand { + private final WorldManager worldManager; + + @Command(name = "parse", description = "Parse a map", ranks = { Rank.BUILDER }, playersOnly = true) + public void onCommand(CommandProvider command) { + Player player = command.getPlayer(); + String[] args = command.getArgs(); + if (args.length < 1) { + player.sendMessage(Style.main("Map", "Usage: /parse ")); + return; + } + int radius = 0; + try { + radius = Integer.parseInt(args[0]); + } catch (NumberFormatException ignored) {} + if (radius <= 0) { + player.sendMessage(Style.error("Map", "§cCannot parse map with a radius of §b" + radius)); + return; + } + MGZWorld mgzWorld = worldManager.getWorld(player.getWorld()); + if (mgzWorld == null) { + player.sendMessage(Style.error("Map", "§cYou cannot parse this map.")); + return; + } + if (!player.isOp()) { + player.sendMessage(Style.error("Map", "§cOnly server operators can parse maps.")); + return; + } + List ids = new ArrayList<>(); + for (String idString : Arrays.stream(args).skip(1).collect(Collectors.toList())) { + int id; + try { + id = Integer.parseInt(idString); + ids.add(id); + } catch (NumberFormatException ignored) {} + } + worldManager.parse(mgzWorld, player.getLocation(), radius, ids); + Bukkit.broadcastMessage(Style.main("Map", "Map §b" + mgzWorld.getName() + " §7is now being parsed!" + + (ids.isEmpty() ? "" : " §7(" + ids.size() + " ids)"))); + } +} \ No newline at end of file diff --git a/buildserver/src/main/java/zone/themcgamer/buildServer/world/WorldManager.java b/buildserver/src/main/java/zone/themcgamer/buildServer/world/WorldManager.java new file mode 100644 index 0000000..4538986 --- /dev/null +++ b/buildserver/src/main/java/zone/themcgamer/buildServer/world/WorldManager.java @@ -0,0 +1,413 @@ +package zone.themcgamer.buildServer.world; + +import lombok.Getter; +import org.apache.commons.io.FileUtils; +import org.bukkit.*; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.block.*; +import org.bukkit.event.entity.EntitySpawnEvent; +import org.bukkit.event.player.PlayerChangedWorldEvent; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.event.weather.WeatherChangeEvent; +import org.bukkit.permissions.PermissionAttachment; +import org.bukkit.plugin.java.JavaPlugin; +import zone.themcgamer.buildServer.Build; +import zone.themcgamer.buildServer.backup.BackupTask; +import zone.themcgamer.buildServer.parse.ParseTask; +import zone.themcgamer.buildServer.parse.command.ParseCommand; +import zone.themcgamer.buildServer.world.command.*; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.core.module.Module; +import zone.themcgamer.core.module.ModuleInfo; +import zone.themcgamer.core.world.MGZWorld; +import zone.themcgamer.core.world.WorldCategory; +import zone.themcgamer.core.world.WorldGenerator; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * @author Braydon + */ +@ModuleInfo(name = "World Manager") @Getter +public class WorldManager extends Module { + private static final String[] permissions = new String[] { + "gopaint.use", + "gobrush.use", + "minecraft.command.difficulty", + "fawe.permpack.basic", + "fawe.voxelbrush", + "fawe.confirm", + "worldedit.*", + "voxelsniper.*", + "builders.util.secretblocks", + "builders.util.banner", + "builders.util.color", + "builders.util.noclip", + "builders.util.nightvision", + "builders.util.advancedfly", + "builders.util.tpgm3", + "headdb.open", + "headdb.phead", + "headdb.free.*", + "headdb.category.*", + "astools.*", + "chars" + }; + + private final List worlds = new ArrayList<>(); + private final List parseTasks = new ArrayList<>(); + private final Map permissionAttachments = new HashMap<>(); + + public WorldManager(JavaPlugin plugin) { + super(plugin); + for (World world : Bukkit.getWorlds()) + setupWorld(world); + // Loading maps from the maps directory + for (WorldCategory category : WorldCategory.values()) { + File categoryDirectory = new File("maps" + File.separator + category.name()); + if (!categoryDirectory.exists()) + continue; + File[] files = categoryDirectory.listFiles(); + if (files == null) + continue; + for (File directory : files) { + if (!directory.isDirectory()) + continue; + try { + worlds.add(new MGZWorld(new File(directory, MGZWorld.FILE_NAME))); + } catch (Exception ex) { + plugin.getLogger().severe("Failed to add world \"" + directory.getPath() + "\""); + ex.printStackTrace(); + } + } + } + // Deleting worlds from the main directory + for (MGZWorld world : worlds) { + String worldName = world.getCategory().name() + "-" + world.getName(); + World bukkitWorld = Bukkit.getWorld(worldName); + if (bukkitWorld != null) + Bukkit.unloadWorld(bukkitWorld, true); + File worldDirectory = new File(Bukkit.getWorldContainer(), worldName); + if (!worldDirectory.exists()) + continue; + File targetDirectory = new File("maps" + File.separator + world.getCategory().name() + File.separator + world.getName()); + if (!targetDirectory.exists()) + targetDirectory.mkdirs(); + copyWorld(worldDirectory, targetDirectory); + FileUtils.deleteQuietly(worldDirectory); + } + // Creating the parse directory and deleting old parsed worlds + File parseDirectory = new File("parse"); + if (!parseDirectory.exists()) + parseDirectory.mkdirs(); + else { + File[] files = parseDirectory.listFiles(); + if (files != null) { + for (File directory : files) { + if (!directory.isDirectory()) + continue; + FileUtils.deleteQuietly(directory); + System.out.println("Deleted old parsed world: " + directory.getPath()); + } + } + } + Bukkit.getScheduler().scheduleSyncRepeatingTask(plugin, () -> { + for (World world : Bukkit.getWorlds()) { + MGZWorld mgzWorld = getWorld(world); + if (mgzWorld != null) { + if (world.getPlayers().isEmpty()) { + setupWorld(world); + Bukkit.unloadWorld(world, true); + unloadWorld(mgzWorld); + mgzWorld.setWorld(null); + new BackupTask(plugin, mgzWorld); + Bukkit.broadcastMessage(Style.main("Map", "Unloading map §b" + mgzWorld.getName())); + } + } + for (Player player : world.getPlayers()) { + if (player.isOp()) + continue; + PermissionAttachment attachment = permissionAttachments.get(player); + if (attachment == null) + continue; + for (String permission : permissions) { + if (mgzWorld == null || (!mgzWorld.hasPrivileges(player))) { + if (player.hasPermission(permission)) + attachment.setPermission(permission, false); + } else if (mgzWorld.hasPrivileges(player) && !player.hasPermission(permission)) + attachment.setPermission(permission, true); + } + } + } + parseTasks.removeIf(parseTask -> { + if (parseTask.isCompleted()) + return true; + parseTask.run(); + return false; + }); + }, 0L, 1L); + registerCommand(new CreateCommand(this)); + registerCommand(new MapsCommand(this)); + registerCommand(new MenuCommand()); + registerCommand(new MapCommand(this)); + registerCommand(new MapInfoCommand(this)); + registerCommand(new AdminCommand(this)); + registerCommand(new AuthorCommand(this)); + registerCommand(new RenameCommand(this)); + registerCommand(new CategoryCommand(this)); + registerCommand(new SaveCommand(this)); + registerCommand(new DeleteCommand(this)); + registerCommand(new ReloadWorldEditCommand()); + + registerCommand(new ParseCommand(this)); + } + + @EventHandler + private void onJoin(PlayerJoinEvent event) { + Player player = event.getPlayer(); + permissionAttachments.put(player, event.getPlayer().addAttachment(getPlugin())); + } + + @EventHandler + private void onPlace(BlockPlaceEvent event) { + Player player = event.getPlayer(); + MGZWorld world = getWorld(player.getWorld()); + if ((world == null && !player.isOp()) || (world != null && (!world.hasPrivileges(player)))) + event.setCancelled(true); + } + + @EventHandler + private void onBreak(BlockBreakEvent event) { + Player player = event.getPlayer(); + MGZWorld world = getWorld(player.getWorld()); + if ((world == null && !player.isOp()) || (world != null && (!world.hasPrivileges(player)))) + event.setCancelled(true); + } + + @EventHandler + private void onInteract(PlayerInteractEvent event) { + Player player = event.getPlayer(); + MGZWorld world = getWorld(player.getWorld()); + if ((world == null && !player.isOp()) || (world != null && (!world.hasPrivileges(player)))) + event.setCancelled(true); + } + + @EventHandler + private void onEntitySpawn(EntitySpawnEvent event) { + if (event.getEntityType() != EntityType.ARMOR_STAND) + event.setCancelled(true); + } + + @EventHandler + private void onIgnite(BlockIgniteEvent event) { + BlockIgniteEvent.IgniteCause cause = event.getCause(); + if (cause == BlockIgniteEvent.IgniteCause.LAVA || cause == BlockIgniteEvent.IgniteCause.SPREAD) + event.setCancelled(true); + } + + @EventHandler + private void onBurn(BlockBurnEvent event) { + event.setCancelled(true); + } + + @EventHandler + private void onSpread(BlockSpreadEvent event) { + event.setCancelled(true); + } + + @EventHandler + private void onBlockFade(BlockFadeEvent event) { + event.setCancelled(true); + } + + @EventHandler + private void onLeafDecay(LeavesDecayEvent event) { + event.setCancelled(true); + } + + @EventHandler + private void onBlockForm(BlockFormEvent event) { + event.setCancelled(true); + } + + @EventHandler + private void onWeatherChange(WeatherChangeEvent event) { + if (event.toWeatherState()) + event.setCancelled(true); + } + + @EventHandler + private void onWorldChange(PlayerChangedWorldEvent event) { + Player player = event.getPlayer(); + MGZWorld toWorld = getWorld(player.getWorld()); + if (toWorld != null) { + player.chat("/mapinfo"); + if (!toWorld.hasPrivileges(player)) { + player.sendMessage(Style.error("Maps", "You do not have privileges to build on this map, " + + "please contact the map author to request build privileges!")); + } + } + DeleteCommand.getConfirmDelete().remove(player); + } + + @EventHandler + private void onQuit(PlayerQuitEvent event) { + Player player = event.getPlayer(); + permissionAttachments.remove(player); + DeleteCommand.getConfirmDelete().remove(player); + } + + public void loadWorld(MGZWorld world) { + if (world == null) + return; + File target = new File(Bukkit.getWorldContainer(),world.getCategory().name() + "-" + world.getName()); + copyWorld(new File("maps" + File.separator + world.getCategory().name() + File.separator + world.getName()), target); + world.setDataFile(new File(target, MGZWorld.FILE_NAME)); + } + + public void unloadWorld(MGZWorld world) { + if (world == null) + return; + File target = new File("maps" + File.separator + world.getCategory().name() + File.separator + world.getName()); + copyWorld(world.getWorld().getWorldFolder(), target); + FileUtils.deleteQuietly(world.getWorld().getWorldFolder()); + world.setDataFile(new File(target, MGZWorld.FILE_NAME)); + } + + public boolean isIllegalName(String name) { + File worldContainer = Bukkit.getWorldContainer(); + File[] files = worldContainer.listFiles(); + if (files != null) { + for (File file : files) { + if (file.getName().equalsIgnoreCase(name)) { + return true; + } + } + } + return false; + } + + public World create(String name, String author, WorldCategory category, WorldGenerator generator, String preset) { + name = name.replaceAll(" ", "_"); + MGZWorld mgzWorld = getWorld(name, category); + if (mgzWorld != null) + throw new IllegalArgumentException("Map with name \"" + name + "\" already exists under category " + category.name()); + if (preset == null) + preset = generator.getPreset(); + if (preset == null) + throw new IllegalArgumentException("Preset is null for generator type " + generator.name()); + + // Setting up the vanilla world + World world = getWorldCreator(category.name() + "-" + name, preset).createWorld(); + setupWorld(world); + world.save(); + + // Creating the MGZWorld + File file = new File(world.getWorldFolder(), MGZWorld.FILE_NAME); + mgzWorld = new MGZWorld(world, file, name, author, preset, category); + mgzWorld.save(); + worlds.add(mgzWorld); + + return world; + } + + public MGZWorld getWorld(World world) { + List worlds = lookup(mgzWorld -> mgzWorld.getWorld() != null && (mgzWorld.getWorld().getName().equals(world.getName()))); + if (worlds.isEmpty()) + return null; + return worlds.get(0); + } + + public MGZWorld getWorld(String name, WorldCategory category) { + List worlds = lookup(mgzWorld -> mgzWorld.getName().equalsIgnoreCase(name) && mgzWorld.getCategory() == category); + if (worlds.isEmpty()) + return null; + return worlds.get(0); + } + + public List getWorld(String name) { + return lookup(world -> world.getName().equalsIgnoreCase(name)); + } + + public List lookup(Predicate predicate) { + return worlds.stream().filter(predicate).collect(Collectors.toList()); + } + + public void parse(MGZWorld mgzWorld, Location center, int radius, List ids) { + if (beingParsed(mgzWorld)) + throw new IllegalStateException("World \"" + mgzWorld.getName() + "\" is already being parsed"); + World world = mgzWorld.getWorld(); + for (Player worldPlayer : world.getPlayers()) + worldPlayer.teleport(Build.INSTANCE.getMainWorld().getSpawnLocation()); + Bukkit.unloadWorld(world, true); + unloadWorld(mgzWorld); + mgzWorld.setWorld(null); + + ParseTask parseTask = new ParseTask(mgzWorld, radius, ids); + parseTasks.add(parseTask); + + try { + File parseWorldDirectory = new File("parse" + File.separator + mgzWorld.getCategory().name() + File.separator + + mgzWorld.getName().replaceAll(" ", "_")); + FileUtils.copyDirectory(new File("maps" + File.separator + mgzWorld.getCategory().name() + File.separator + mgzWorld.getName()), parseWorldDirectory); + World parseWorld = getWorldCreator(parseWorldDirectory.getPath(), mgzWorld.getPreset()).createWorld(); + setupWorld(parseWorld); + center.setWorld(parseWorld); + parseTask.start(parseWorld, center); + } catch (IOException ex) { + ex.printStackTrace(); + } + } + + public boolean beingParsed(MGZWorld world) { + return parseTasks.stream().anyMatch(parseTask -> parseTask.getMgzWorld().equals(world)); + } + + public void copyWorld(File oldDirectory, File newDirectory) { + try { + FileUtils.copyDirectory(oldDirectory, new File(newDirectory.getPath().replaceAll(" ", "_"))); + } catch (IOException ex) { + ex.printStackTrace(); + } + } + + public WorldCreator getWorldCreator(String name, String preset) { + WorldCreator creator = new WorldCreator(name); + creator.environment(World.Environment.NORMAL); + creator.type(WorldType.FLAT); + if (preset != null) + creator.generatorSettings(preset); + creator.generateStructures(false); + return creator; + } + + public void setupWorld(World world) { + long time = 6000L; + if (world.getName().toLowerCase().contains("christmas")) + time = 12000L; + else if (world.getName().toLowerCase().contains("halloween")) + time = 17000L; + world.setTime(time); + world.setThundering(false); + world.setStorm(false); + world.setSpawnLocation(0, 150, 0); + world.setGameRuleValue("randomTickSpeed", "0"); + world.setGameRuleValue("doDaylightCycle", "false"); + world.setGameRuleValue("showDeathMessages", "false"); + world.setGameRuleValue("doFireTick", "false"); + world.setGameRuleValue("mobGriefing", "false"); + world.setGameRuleValue("doMobLoot", "false"); + world.setGameRuleValue("doMobSpawning", "false"); + } +} \ No newline at end of file diff --git a/buildserver/src/main/java/zone/themcgamer/buildServer/world/command/AdminCommand.java b/buildserver/src/main/java/zone/themcgamer/buildServer/world/command/AdminCommand.java new file mode 100644 index 0000000..1b1c485 --- /dev/null +++ b/buildserver/src/main/java/zone/themcgamer/buildServer/world/command/AdminCommand.java @@ -0,0 +1,58 @@ +package zone.themcgamer.buildServer.world.command; + +import lombok.AllArgsConstructor; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import zone.themcgamer.buildServer.world.WorldManager; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.core.world.MGZWorld; +import zone.themcgamer.data.Rank; + +/** + * @author Braydon + */ +@AllArgsConstructor +public class AdminCommand { + private final WorldManager worldManager; + + @Command(name = "admin", description = "Add or remove an admin from a map", ranks = { Rank.BUILDER }, playersOnly = true) + public void onCommand(CommandProvider command) { + Player player = command.getPlayer(); + String[] args = command.getArgs(); + if (args.length < 1) { + player.sendMessage(Style.main("Map", "Usage: /admin ")); + return; + } + MGZWorld world = worldManager.getWorld(player.getWorld()); + if (world == null) { + player.sendMessage(Style.error("Map", "§cYou cannot modify the admins of this map.")); + return; + } + if (!world.getOriginalCreator().equals(player.getName()) && !player.isOp()) { + player.sendMessage(Style.error("Map", "§cYou cannot modify the admins of this map.")); + return; + } + if (args[0].equalsIgnoreCase(world.getOriginalCreator())) { + player.sendMessage(Style.error("Map", "§cThe original author can't have their admin privileges modified.")); + return; + } + if (worldManager.beingParsed(world)) { + player.sendMessage(Style.error("Map", "§cThis map is currently being parsed, changing the admins list has been disabled")); + return; + } + Player target = Bukkit.getPlayer(args[0]); + if (world.getAdmins().remove(args[0].toLowerCase())) { + player.sendMessage(Style.main("Map", "§b" + args[0] + " §7is no-longer an admin on §6" + world.getName())); + if (target != null && (!player.equals(target))) + target.sendMessage(Style.main("Map", "§cYour admin privileges on §b" + world.getName() + " §cwere removed")); + } else { + world.getAdmins().add(args[0].toLowerCase()); + player.sendMessage(Style.main("Map", "§b" + args[0] + " §7is now an admin on §6" + world.getName())); + if (target != null && (!player.equals(target))) + target.sendMessage(Style.main("Map", "§aYou were given admin privileges on §b" + world.getName())); + } + world.save(); + } +} \ No newline at end of file diff --git a/buildserver/src/main/java/zone/themcgamer/buildServer/world/command/AuthorCommand.java b/buildserver/src/main/java/zone/themcgamer/buildServer/world/command/AuthorCommand.java new file mode 100644 index 0000000..e31944d --- /dev/null +++ b/buildserver/src/main/java/zone/themcgamer/buildServer/world/command/AuthorCommand.java @@ -0,0 +1,44 @@ +package zone.themcgamer.buildServer.world.command; + +import lombok.AllArgsConstructor; +import org.bukkit.entity.Player; +import zone.themcgamer.buildServer.world.WorldManager; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.core.world.MGZWorld; +import zone.themcgamer.data.Rank; + +/** + * @author Braydon + */ +@AllArgsConstructor +public class AuthorCommand { + private final WorldManager worldManager; + + @Command(name = "author", description = "Set the author of a map", ranks = { Rank.BUILDER }, playersOnly = true) + public void onCommand(CommandProvider command) { + Player player = command.getPlayer(); + String[] args = command.getArgs(); + if (args.length < 1) { + player.sendMessage(Style.main("Map", "Usage: /author ")); + return; + } + MGZWorld world = worldManager.getWorld(player.getWorld()); + if (world == null) { + player.sendMessage(Style.error("Map", "§cYou cannot update the author of this map.")); + return; + } + if (!world.getOriginalCreator().equals(player.getName()) && !player.isOp()) { + player.sendMessage(Style.error("Map", "§cYou cannot update the author of this map.")); + return; + } + if (worldManager.beingParsed(world)) { + player.sendMessage(Style.error("Map", "§cThis map is currently being parsed, changing the author has been disabled")); + return; + } + world.setAuthor(String.join(" ", args)); + world.save(); + player.sendMessage(Style.main("Map", "Map author set to §b" + world.getAuthor())); + } +} \ No newline at end of file diff --git a/buildserver/src/main/java/zone/themcgamer/buildServer/world/command/CategoryCommand.java b/buildserver/src/main/java/zone/themcgamer/buildServer/world/command/CategoryCommand.java new file mode 100644 index 0000000..5b78159 --- /dev/null +++ b/buildserver/src/main/java/zone/themcgamer/buildServer/world/command/CategoryCommand.java @@ -0,0 +1,81 @@ +package zone.themcgamer.buildServer.world.command; + +import lombok.AllArgsConstructor; +import org.apache.commons.io.FileUtils; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.entity.Player; +import zone.themcgamer.buildServer.Build; +import zone.themcgamer.buildServer.world.WorldManager; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.core.world.MGZWorld; +import zone.themcgamer.core.world.WorldCategory; +import zone.themcgamer.data.Rank; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.stream.Collectors; + +/** + * @author Braydon + */ +@AllArgsConstructor +public class CategoryCommand { + private final WorldManager worldManager; + + @Command(name = "category", description = "Set the category of a map", ranks = { Rank.BUILDER }, playersOnly = true) + public void onCommand(CommandProvider command) { + Player player = command.getPlayer(); + String[] args = command.getArgs(); + if (args.length < 1) { + player.sendMessage(Style.main("Map", "Usage: /category ")); + return; + } + WorldCategory category = WorldCategory.lookup(String.join(" ", args)); + if (category == null) { + player.sendMessage(Style.error("Map", "§cInvalid category: §f" + Arrays.stream(WorldCategory.values()) + .map(WorldCategory::getName).collect(Collectors.joining("§7, §f")))); + return; + } + MGZWorld mgzWorld = worldManager.getWorld(player.getWorld()); + World world; + if (mgzWorld == null || ((world = mgzWorld.getWorld()) == null)) { + player.sendMessage(Style.error("Map", "§cYou cannot update the category of this map.")); + return; + } + if (!mgzWorld.getOriginalCreator().equals(player.getName()) && !player.isOp()) { + player.sendMessage(Style.error("Map", "§cYou cannot update the category of this map.")); + return; + } + if (worldManager.beingParsed(mgzWorld)) { + player.sendMessage(Style.error("Map", "§cThis map is currently being parsed, changing the category has been disabled")); + return; + } + if (worldManager.getWorld(mgzWorld.getName(), category) != null) { + player.sendMessage(Style.error("Map", "§cThere is already a map with that category")); + return; + } + for (Player worldPlayer : world.getPlayers()) { + worldPlayer.teleport(Build.INSTANCE.getMainWorld().getSpawnLocation()); + worldPlayer.sendMessage(Style.main("Map", "Map category set to §b" + category.getName())); + } + try { + Bukkit.unloadWorld(world, true); + mgzWorld.setWorld(null); + + WorldCategory oldCategory = mgzWorld.getCategory(); + mgzWorld.setCategory(category); + mgzWorld.save(); + + File newDirectory = new File("maps" + File.separator + category.name() + File.separator + mgzWorld.getName()); + FileUtils.moveDirectory(new File(oldCategory.name() + "-" + mgzWorld.getName()), newDirectory); + mgzWorld.setDataFile(new File(newDirectory, MGZWorld.FILE_NAME)); + FileUtils.deleteQuietly(new File("maps" + File.separator + oldCategory.name() + File.separator + mgzWorld.getName())); + } catch (IOException ex) { + ex.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/buildserver/src/main/java/zone/themcgamer/buildServer/world/command/CreateCommand.java b/buildserver/src/main/java/zone/themcgamer/buildServer/world/command/CreateCommand.java new file mode 100644 index 0000000..e2e6ea8 --- /dev/null +++ b/buildserver/src/main/java/zone/themcgamer/buildServer/world/command/CreateCommand.java @@ -0,0 +1,56 @@ +package zone.themcgamer.buildServer.world.command; + +import lombok.AllArgsConstructor; +import org.bukkit.World; +import org.bukkit.entity.Player; +import zone.themcgamer.buildServer.world.WorldManager; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.core.world.MGZWorld; +import zone.themcgamer.core.world.WorldCategory; +import zone.themcgamer.core.world.WorldGenerator; +import zone.themcgamer.data.Rank; + +/** + * @author Braydon + */ +@AllArgsConstructor +public class CreateCommand { + private final WorldManager worldManager; + + @Command(name = "create", description = "Create a new map", ranks = { Rank.BUILDER }, playersOnly = true) + public void onCommand(CommandProvider command) { + Player player = command.getPlayer(); + String[] args = command.getArgs(); + if (args.length < 1) { + player.sendMessage(Style.main("Map", "Usage: /create [generator|-v]")); + player.sendMessage(Style.main("Map", "§6generator §7= A custom generator, see here: https://www.minecraft101.net/superflat/")); + player.sendMessage(Style.main("Map", "§6-v §7= void")); + return; + } + MGZWorld mgzWorld = worldManager.getWorld(args[0], WorldCategory.OTHER); + if (mgzWorld != null) { + player.sendMessage(Style.error("Map", "§cThere is already a map with that name")); + return; + } + String name = args[0]; + if (worldManager.isIllegalName(name)) { + player.sendMessage(Style.error("Map", "§cIllegal map name!")); + return; + } + WorldGenerator generator = WorldGenerator.FLAT; + String preset = null; + if (args.length >= 2) { + if (args[1].equalsIgnoreCase("-v")) + generator = WorldGenerator.VOID; + else { + generator = WorldGenerator.CUSTOM; + preset = args[1].toLowerCase(); + } + } + World world = worldManager.create(name, player.getName(), WorldCategory.OTHER, generator, preset); + player.teleport(world.getSpawnLocation()); + player.sendMessage(Style.main("Map", "Created a new map named §b" + name + "§7!")); + } +} \ No newline at end of file diff --git a/buildserver/src/main/java/zone/themcgamer/buildServer/world/command/DeleteCommand.java b/buildserver/src/main/java/zone/themcgamer/buildServer/world/command/DeleteCommand.java new file mode 100644 index 0000000..5a21247 --- /dev/null +++ b/buildserver/src/main/java/zone/themcgamer/buildServer/world/command/DeleteCommand.java @@ -0,0 +1,63 @@ +package zone.themcgamer.buildServer.world.command; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.apache.commons.io.FileUtils; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.entity.Player; +import zone.themcgamer.buildServer.Build; +import zone.themcgamer.buildServer.world.WorldManager; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.core.world.MGZWorld; +import zone.themcgamer.data.Rank; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Braydon + */ +@AllArgsConstructor +public class DeleteCommand { + @Getter private static final List confirmDelete = new ArrayList<>(); + + private final WorldManager worldManager; + + @Command(name = "delete", description = "Delete a map", ranks = { Rank.BUILDER }, playersOnly = true) + public void onCommand(CommandProvider command) { + Player player = command.getPlayer(); + MGZWorld mgzWorld = worldManager.getWorld(player.getWorld()); + World world; + if (mgzWorld == null || ((world = mgzWorld.getWorld()) == null)) { + player.sendMessage(Style.error("Map", "§cYou cannot delete this map.")); + return; + } + if (!mgzWorld.getOriginalCreator().equals(player.getName()) && !player.isOp()) { + player.sendMessage(Style.error("Map", "§cYou cannot delete this map.")); + return; + } + if (worldManager.beingParsed(mgzWorld)) { + player.sendMessage(Style.error("Map", "§cThis map is currently being parsed, deleting has been disabled")); + return; + } + if (confirmDelete.remove(player)) { + for (Player worldPlayer : world.getPlayers()) { + worldPlayer.teleport(Build.INSTANCE.getMainWorld().getSpawnLocation()); + worldPlayer.sendMessage(Style.main("Map", "Map §b" + mgzWorld.getName() + " §7was deleted")); + } + Bukkit.unloadWorld(world, true); + FileUtils.deleteQuietly(world.getWorldFolder()); + mgzWorld.setWorld(null); + worldManager.getWorlds().remove(mgzWorld); + FileUtils.deleteQuietly(new File("maps" + File.separator + mgzWorld.getCategory().name() + File.separator + mgzWorld.getName())); + } else { + confirmDelete.add(player); + player.sendMessage(Style.main("Map", "Execute this command again to confirm deletion of map §b" + mgzWorld.getName())); + player.sendMessage(Style.main("Map", "§c§lNOTE §f- §7This action CANNOT be undone")); + } + } +} \ No newline at end of file diff --git a/buildserver/src/main/java/zone/themcgamer/buildServer/world/command/MapCommand.java b/buildserver/src/main/java/zone/themcgamer/buildServer/world/command/MapCommand.java new file mode 100644 index 0000000..2629658 --- /dev/null +++ b/buildserver/src/main/java/zone/themcgamer/buildServer/world/command/MapCommand.java @@ -0,0 +1,74 @@ +package zone.themcgamer.buildServer.world.command; + +import lombok.AllArgsConstructor; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import zone.themcgamer.buildServer.world.WorldManager; +import zone.themcgamer.common.EnumUtils; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.core.command.TabComplete; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.core.world.MGZWorld; +import zone.themcgamer.core.world.WorldCategory; +import zone.themcgamer.data.Rank; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author Braydon + */ +@AllArgsConstructor +public class MapCommand { + private final WorldManager worldManager; + + @Command(name = "map", description = "Teleport to a map", ranks = { Rank.BUILDER }, playersOnly = true) + public void onCommand(CommandProvider command) { + Player player = command.getPlayer(); + String[] args = command.getArgs(); + if (args.length < 1) { + player.sendMessage(Style.main("Map", "Usage: /map [category]")); + return; + } + List worlds = worldManager.getWorld(args[0]); + if (worlds.isEmpty()) { + player.sendMessage(Style.error("Map", "§cThere is no map with that name")); + return; + } + MGZWorld world; + if (worlds.size() == 1) + world = worlds.get(0); + else { + WorldCategory category = null; + if (args.length >= 2) + category = EnumUtils.fromString(WorldCategory.class, args[1].toUpperCase()); + if (category == null) { + player.sendMessage(Style.error("Map", "§cYou either didn't specify a category, or the given category is incorrect!")); + return; + } + world = worldManager.getWorld(args[0], category); + } + if (world == null) { + player.sendMessage(Style.error("Map", "§cFailed to locate world, please wait a few moments and try again")); + return; + } + if (worldManager.beingParsed(world)) { + player.sendMessage(Style.error("Map", "§cThis map is currently being parsed, teleportation has been disabled")); + return; + } + if (world.getWorld() == null) { + worldManager.loadWorld(world); + world.setWorld(worldManager.getWorldCreator(world.getCategory().name() + "-" + world.getName(), world.getPreset()).createWorld()); + worldManager.setupWorld(world.getWorld()); + Bukkit.broadcastMessage(Style.main("Map", "Loaded world §b" + world.getName())); + } + player.teleport(world.getWorld().getSpawnLocation()); + player.sendMessage(Style.main("Map", "Teleported to §b" + world.getName())); + } + + @TabComplete(name = "map") + public List onTab(CommandProvider command) { + return worldManager.getWorlds().stream().map(MGZWorld::getName).collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/buildserver/src/main/java/zone/themcgamer/buildServer/world/command/MapInfoCommand.java b/buildserver/src/main/java/zone/themcgamer/buildServer/world/command/MapInfoCommand.java new file mode 100644 index 0000000..cca8edf --- /dev/null +++ b/buildserver/src/main/java/zone/themcgamer/buildServer/world/command/MapInfoCommand.java @@ -0,0 +1,33 @@ +package zone.themcgamer.buildServer.world.command; + +import lombok.AllArgsConstructor; +import org.bukkit.entity.Player; +import zone.themcgamer.buildServer.world.WorldManager; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.core.world.MGZWorld; +import zone.themcgamer.data.Rank; + +/** + * @author Braydon + */ +@AllArgsConstructor +public class MapInfoCommand { + private final WorldManager worldManager; + + @Command(name = "mapinfo", description = "View information about a map", ranks = { Rank.BUILDER }, playersOnly = true) + public void onCommand(CommandProvider command) { + Player player = command.getPlayer(); + MGZWorld world = worldManager.getWorld(player.getWorld()); + if (world == null) { + player.sendMessage(Style.error("Map", "§cYou cannot view information for this map.")); + return; + } + player.sendMessage(Style.main("Map", "Information for §b" + world.getName() + "§7:")); + player.sendMessage(" §8- §7Author §f" + world.getAuthor() + (world.getAuthor().equals(world.getOriginalCreator()) ? "" : " §7(original: " + world.getOriginalCreator() + ")")); + player.sendMessage(" §8- §7Preset §f" + world.getPreset()); + player.sendMessage(" §8- §7Category §f" + world.getCategory().getName()); + player.sendMessage(" §8- §7Admins §f" + (world.getAdmins().isEmpty() ? "None" : String.join("§7, §f", world.getAdmins()))); + } +} \ No newline at end of file diff --git a/buildserver/src/main/java/zone/themcgamer/buildServer/world/command/MapsCommand.java b/buildserver/src/main/java/zone/themcgamer/buildServer/world/command/MapsCommand.java new file mode 100644 index 0000000..f810958 --- /dev/null +++ b/buildserver/src/main/java/zone/themcgamer/buildServer/world/command/MapsCommand.java @@ -0,0 +1,48 @@ +package zone.themcgamer.buildServer.world.command; + +import lombok.AllArgsConstructor; +import org.bukkit.command.CommandSender; +import zone.themcgamer.buildServer.world.WorldManager; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.core.world.MGZWorld; +import zone.themcgamer.core.world.WorldCategory; +import zone.themcgamer.data.Rank; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @author Braydon + */ +@AllArgsConstructor +public class MapsCommand { + private final WorldManager worldManager; + + @Command(name = "maps", description = "List all maps", ranks = { Rank.BUILDER }) + public void onCommand(CommandProvider command) { + CommandSender sender = command.getSender(); + Map> worldsMap = new HashMap<>(); + for (MGZWorld world : worldManager.getWorlds()) { + List worlds = worldsMap.getOrDefault(world.getCategory(), new ArrayList<>()); + worlds.add(world); + worldsMap.put(world.getCategory(), worlds); + } + if (worldsMap.isEmpty()) { + sender.sendMessage(Style.main("Map", "There are no maps to view.")); + return; + } + int maps = 0; + for (List list : worldsMap.values()) + maps+= list.size(); + sender.sendMessage(Style.main("Maps", "Showing §b" + maps + " §7maps")); + for (Map.Entry> entry : worldsMap.entrySet()) { + sender.sendMessage(Style.color("&6" + entry.getKey().getName() + " &8» &f" + + entry.getValue().stream().map(MGZWorld::getName).collect(Collectors.joining("§7, §f")))); + } + } +} \ No newline at end of file diff --git a/buildserver/src/main/java/zone/themcgamer/buildServer/world/command/MenuCommand.java b/buildserver/src/main/java/zone/themcgamer/buildServer/world/command/MenuCommand.java new file mode 100644 index 0000000..83d842f --- /dev/null +++ b/buildserver/src/main/java/zone/themcgamer/buildServer/world/command/MenuCommand.java @@ -0,0 +1,14 @@ +package zone.themcgamer.buildServer.world.command; + +import zone.themcgamer.buildServer.world.menu.BuildManagerMenu; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.data.Rank; + +public class MenuCommand { + @Command(name = "menu", aliases = { "gui", "buildmanager", "bm" } , description = "Open the build management menu.", + ranks = { Rank.BUILDER }, playersOnly = true) + public void onCommand(CommandProvider command) { + new BuildManagerMenu(command.getPlayer()).open(); + } +} diff --git a/buildserver/src/main/java/zone/themcgamer/buildServer/world/command/ReloadWorldEditCommand.java b/buildserver/src/main/java/zone/themcgamer/buildServer/world/command/ReloadWorldEditCommand.java new file mode 100644 index 0000000..652b59c --- /dev/null +++ b/buildserver/src/main/java/zone/themcgamer/buildServer/world/command/ReloadWorldEditCommand.java @@ -0,0 +1,21 @@ +package zone.themcgamer.buildServer.world.command; + +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.data.Rank; + +/** + * @author Braydon + */ +public class ReloadWorldEditCommand { + @Command(name = "reloadworldedit", aliases = { "reloadwe", "rwe" }, description = "Reload WorldEdit", ranks = { Rank.BUILDER }) + public void onCommand(CommandProvider command) { + Plugin plugin = Bukkit.getPluginManager().getPlugin("WorldEdit"); + plugin.onDisable(); + plugin.onEnable(); + Bukkit.broadcastMessage(Style.main("Map", "WorldEdit was reloaded")); + } +} \ No newline at end of file diff --git a/buildserver/src/main/java/zone/themcgamer/buildServer/world/command/RenameCommand.java b/buildserver/src/main/java/zone/themcgamer/buildServer/world/command/RenameCommand.java new file mode 100644 index 0000000..6b10b3b --- /dev/null +++ b/buildserver/src/main/java/zone/themcgamer/buildServer/world/command/RenameCommand.java @@ -0,0 +1,82 @@ +package zone.themcgamer.buildServer.world.command; + +import lombok.AllArgsConstructor; +import org.apache.commons.io.FileUtils; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.entity.Player; +import zone.themcgamer.buildServer.Build; +import zone.themcgamer.buildServer.world.WorldManager; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.core.world.MGZWorld; +import zone.themcgamer.data.Rank; + +import java.io.File; +import java.io.IOException; + +/** + * @author Braydon + */ +@AllArgsConstructor +public class RenameCommand { + private final WorldManager worldManager; + + @Command(name = "name", description = "Rename a map", ranks = { Rank.BUILDER }, playersOnly = true) + public void onCommand(CommandProvider command) { + Player player = command.getPlayer(); + String[] args = command.getArgs(); + if (args.length < 1) { + player.sendMessage(Style.main("Map", "Usage: /name ")); + return; + } + MGZWorld mgzWorld = worldManager.getWorld(player.getWorld()); + World world; + if (mgzWorld == null || ((world = mgzWorld.getWorld()) == null)) { + player.sendMessage(Style.error("Map", "§cYou cannot rename this map.")); + return; + } + if (!mgzWorld.getOriginalCreator().equals(player.getName()) && !player.isOp()) { + player.sendMessage(Style.error("Map", "§cYou cannot rename this map.")); + return; + } + String mapName = args[0]; + if (worldManager.isIllegalName(mapName)) { + player.sendMessage(Style.error("Map", "§cIllegal map name!")); + return; + } + if (worldManager.beingParsed(mgzWorld)) { + player.sendMessage(Style.error("Map", "§cThis map is currently being parsed, changing the name has been disabled")); + return; + } + String name = String.join(" ", args).replaceAll(" ", "_"); + if (worldManager.isIllegalName(name)) { + player.sendMessage(Style.error("Map", "§cIllegal map name!")); + return; + } + if (worldManager.getWorld(name, mgzWorld.getCategory()) != null) { + player.sendMessage(Style.error("Map", "§cThere is already a map with that name")); + return; + } + for (Player worldPlayer : world.getPlayers()) { + worldPlayer.teleport(Build.INSTANCE.getMainWorld().getSpawnLocation()); + worldPlayer.sendMessage(Style.main("Map", "Map name set to §b" + name)); + } + try { + Bukkit.unloadWorld(world, true); + mgzWorld.setWorld(null); + + String oldName = mgzWorld.getName(); + mgzWorld.setName(name); + mgzWorld.save(); + + File newDirectory = new File("maps" + File.separator + mgzWorld.getCategory().name() + File.separator + name); + FileUtils.moveDirectory(new File(mgzWorld.getCategory().name() + "-" + oldName), newDirectory); + FileUtils.deleteQuietly(new File("maps" + File.separator + mgzWorld.getCategory().name() + File.separator + oldName)); + mgzWorld.setDataFile(new File(newDirectory, MGZWorld.FILE_NAME)); + } catch (IOException ex) { + ex.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/buildserver/src/main/java/zone/themcgamer/buildServer/world/command/SaveCommand.java b/buildserver/src/main/java/zone/themcgamer/buildServer/world/command/SaveCommand.java new file mode 100644 index 0000000..24a9a02 --- /dev/null +++ b/buildserver/src/main/java/zone/themcgamer/buildServer/world/command/SaveCommand.java @@ -0,0 +1,36 @@ +package zone.themcgamer.buildServer.world.command; + +import lombok.RequiredArgsConstructor; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import zone.themcgamer.buildServer.world.WorldManager; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.core.world.MGZWorld; +import zone.themcgamer.data.Rank; + +import java.io.File; + +@RequiredArgsConstructor +public class SaveCommand { + private final WorldManager worldManager; + + @Command(name = "save", aliases = { "saveworld", "sw" }, description = "Save your world", ranks = { Rank.BUILDER }, playersOnly = true) + public void onCommand(CommandProvider command) { + Player player = command.getPlayer(); + player.getWorld().save(); + + MGZWorld mgzWorld = worldManager.getWorld(player.getWorld()); + if (mgzWorld == null) + return; + + if (!mgzWorld.hasPrivileges(player)) { + player.sendMessage(Style.error("Map", "§cYou cannot save this map.")); + return; + } + + Bukkit.broadcastMessage(Style.main("Map", "Saved the map &b" + mgzWorld.getName() + "&7!")); + worldManager.copyWorld(player.getWorld().getWorldFolder(), new File("maps" + File.separator + mgzWorld.getCategory().name() + File.separator + mgzWorld.getName())); + } +} diff --git a/buildserver/src/main/java/zone/themcgamer/buildServer/world/menu/BuildManagerMenu.java b/buildserver/src/main/java/zone/themcgamer/buildServer/world/menu/BuildManagerMenu.java new file mode 100644 index 0000000..365bc81 --- /dev/null +++ b/buildserver/src/main/java/zone/themcgamer/buildServer/world/menu/BuildManagerMenu.java @@ -0,0 +1,46 @@ +package zone.themcgamer.buildServer.world.menu; + +import com.cryptomorin.xseries.XMaterial; +import org.bukkit.entity.Player; +import zone.themcgamer.buildServer.world.WorldManager; +import zone.themcgamer.core.common.ItemBuilder; +import zone.themcgamer.core.common.menu.Button; +import zone.themcgamer.core.common.menu.Menu; +import zone.themcgamer.core.common.menu.MenuType; +import zone.themcgamer.core.module.Module; +import zone.themcgamer.core.world.MGZWorld; + +public class BuildManagerMenu extends Menu { + public BuildManagerMenu(Player player) { + super(player, "Build Manager", 1, MenuType.CHEST); + } + + @Override + protected void onOpen() { + set(0, new Button(new ItemBuilder(XMaterial.LEAD) + .setName("§6My Maps").setLore( + "", + "§7Click to view the worlds you have access to" + ).toItemStack(), event -> { + new MapsMenu(player, null).open(); + })); + + set(1, new Button(new ItemBuilder(XMaterial.FILLED_MAP) + .setName("§6All Maps").setLore( + "", + "§7Click to view all maps" + ).toItemStack(), event -> { + new MapsCategoryMenu(player).open(); + })); + + MGZWorld world; + WorldManager worldManager = Module.getModule(WorldManager.class); + if (worldManager == null || ((world = worldManager.getWorld(player.getWorld())) == null || !world.hasPrivileges(player))) + return; + set(8, new Button(new ItemBuilder(world.getCategory().getIcon()) + .setName("§6" + world.getName()).setLore( + "", + "§7Click to manage your map!" + ).toItemStack())); + } +} diff --git a/buildserver/src/main/java/zone/themcgamer/buildServer/world/menu/MapsCategoryMenu.java b/buildserver/src/main/java/zone/themcgamer/buildServer/world/menu/MapsCategoryMenu.java new file mode 100644 index 0000000..f874194 --- /dev/null +++ b/buildserver/src/main/java/zone/themcgamer/buildServer/world/menu/MapsCategoryMenu.java @@ -0,0 +1,40 @@ +package zone.themcgamer.buildServer.world.menu; + +import org.bukkit.Material; +import org.bukkit.entity.Player; +import zone.themcgamer.buildServer.world.WorldManager; +import zone.themcgamer.core.common.ItemBuilder; +import zone.themcgamer.core.common.menu.Button; +import zone.themcgamer.core.common.menu.Menu; +import zone.themcgamer.core.common.menu.MenuType; +import zone.themcgamer.core.module.Module; +import zone.themcgamer.core.world.WorldCategory; + +public class MapsCategoryMenu extends Menu { + public MapsCategoryMenu(Player player) { + super(player, "Select Category", 3, MenuType.CHEST); + } + + @Override + protected void onOpen() { + WorldManager worldManager = Module.getModule(WorldManager.class); + if (worldManager == null) + return; + for (WorldCategory worldCategory : WorldCategory.values()) { + long worlds = worldManager.getWorlds().stream().filter(world -> world.getCategory() == worldCategory).count(); + add(new Button(new ItemBuilder(worldCategory.getIcon()) + .setName("§b§l" + worldCategory.getName()) + .setLore( + "§6" + worlds + " §7map" + (worlds == 1 ? "" : "s") + " in this category!", + "", + "§aClick to view this category." + ).toItemStack(), event -> { + if (worlds <= 0L) + return; + new MapsMenu(player, worldCategory).open(); + })); + } + set(2, 0, new Button(new ItemBuilder(Material.ARROW) + .setName("§c« Go Back").toItemStack(), event -> new BuildManagerMenu(player).open())); + } +} diff --git a/buildserver/src/main/java/zone/themcgamer/buildServer/world/menu/MapsMenu.java b/buildserver/src/main/java/zone/themcgamer/buildServer/world/menu/MapsMenu.java new file mode 100644 index 0000000..de427b9 --- /dev/null +++ b/buildserver/src/main/java/zone/themcgamer/buildServer/world/menu/MapsMenu.java @@ -0,0 +1,56 @@ +package zone.themcgamer.buildServer.world.menu; + +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.Nullable; +import zone.themcgamer.buildServer.world.WorldManager; +import zone.themcgamer.core.common.ItemBuilder; +import zone.themcgamer.core.common.menu.Button; +import zone.themcgamer.core.common.menu.Menu; +import zone.themcgamer.core.common.menu.MenuType; +import zone.themcgamer.core.module.Module; +import zone.themcgamer.core.world.MGZWorld; +import zone.themcgamer.core.world.WorldCategory; + +import java.util.List; +import java.util.stream.Collectors; + +public class MapsMenu extends Menu { + private final WorldCategory category; + + public MapsMenu(Player player, @Nullable WorldCategory category) { + super(player, category == null ? "My Maps" : "Maps - " + category.getName(), 6, MenuType.CHEST); + this.category = category; + } + + @Override + protected void onOpen() { + WorldManager worldManager = Module.getModule(WorldManager.class); + if (worldManager == null) + return; + List worlds = worldManager.getWorlds().stream() + .filter(world -> category == null ? world.hasPrivileges(player) : world.getCategory() == category) + .sorted((a, b) -> Boolean.compare(a.getOriginalCreator().equals(player.getName()), b.getOriginalCreator().equals(player.getName()))) + .collect(Collectors.toList()); + for (MGZWorld world : worlds) { + add(new Button(new ItemBuilder(world.getCategory().getIcon()) + .setName("§a" + world.getName() + (world.hasPrivileges(player) ? " §c(Access)" : "")).setLore( + "", + " §8- §7Author §f" + world.getAuthor() + (world.getAuthor().equals(world.getOriginalCreator()) ? "" : " §7(original: " + world.getOriginalCreator() + ")"), + " §8- §7Preset §f" + world.getPreset(), + " §8- §7Category §f" + world.getCategory().getName(), + " §8- §7Admins §f" + (world.getAdmins().isEmpty() ? "None" : String.join("§7, §f", world.getAdmins())), + "", + "§aClick to teleport to this map." + ).toItemStack(), event -> { + player.chat(String.format("/map %s %s", world.getName(), world.getCategory().name())); + })); + } + set(5, 0, new Button(new ItemBuilder(Material.ARROW) + .setName("§c« Go Back").toItemStack(), event -> { + if (category == null) + new BuildManagerMenu(player).open(); + else new MapsCategoryMenu(player).open(); + })); + } +} \ No newline at end of file diff --git a/buildserver/src/main/resources/plugin.yml b/buildserver/src/main/resources/plugin.yml new file mode 100644 index 0000000..98fe7be --- /dev/null +++ b/buildserver/src/main/resources/plugin.yml @@ -0,0 +1,5 @@ +name: Build +version: 1.0-SNAPSHOT +api-version: 1.13 +main: zone.themcgamer.buildServer.Build +author: MGZ Development Team \ No newline at end of file diff --git a/commons/build.gradle.kts b/commons/build.gradle.kts new file mode 100644 index 0000000..281f896 --- /dev/null +++ b/commons/build.gradle.kts @@ -0,0 +1 @@ +dependencies {} \ No newline at end of file diff --git a/commons/src/main/java/zone/themcgamer/common/BuildData.java b/commons/src/main/java/zone/themcgamer/common/BuildData.java new file mode 100644 index 0000000..146f4e5 --- /dev/null +++ b/commons/src/main/java/zone/themcgamer/common/BuildData.java @@ -0,0 +1,52 @@ +package zone.themcgamer.common; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +/** + * @author Braydon + * @implNote This class holds data for the current build + */ +@AllArgsConstructor @Getter @ToString +public class BuildData { + @Getter private static BuildData build; + + static { + InputStream inputStream = BuildData.class.getClassLoader().getResourceAsStream("git.properties"); + if (inputStream != null) { + try { + Properties properties = new Properties(); + properties.load(inputStream); + build = new BuildData( + properties.getProperty("git.branch"), + properties.getProperty("git.build.host"), + properties.getProperty("git.build.user.email"), + properties.getProperty("git.build.user.name"), + properties.getProperty("git.build.version"), + properties.getProperty("git.commit.id"), + properties.getProperty("git.commit.id.abbrev"), + properties.getProperty("git.commit.message.full"), + properties.getProperty("git.commit.message.short"), + properties.getProperty("git.commit.time"), + properties.getProperty("insane_module") + ); + } catch (IOException ex) { + ex.printStackTrace(); + } finally { + try { + inputStream.close(); + } catch (IOException ex) { + ex.printStackTrace(); + } + } + } + } + + private final String branch, host, email, username, version, commitId, commitIdAbbreviation, + commitMessageFull, commitMessageShort, time, module; +} \ No newline at end of file diff --git a/commons/src/main/java/zone/themcgamer/common/CpuMonitor.java b/commons/src/main/java/zone/themcgamer/common/CpuMonitor.java new file mode 100644 index 0000000..4bdfe2a --- /dev/null +++ b/commons/src/main/java/zone/themcgamer/common/CpuMonitor.java @@ -0,0 +1,158 @@ +package zone.themcgamer.common; + +import javax.management.JMX; +import javax.management.MBeanServer; +import javax.management.ObjectName; +import java.lang.management.ManagementFactory; +import java.math.BigDecimal; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * @author Lucko + * credits = https://github.com/lucko/spark + */ +public enum CpuMonitor { + ; + + /** The object name of the com.sun.management.OperatingSystemMXBean */ + private static final String OPERATING_SYSTEM_BEAN = "java.lang:type=OperatingSystem"; + /** The OperatingSystemMXBean instance */ + private static final OperatingSystemMXBean BEAN; + /** The executor used to monitor & calculate rolling averages. */ + private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor(r -> { + Thread thread = Executors.defaultThreadFactory().newThread(r); + thread.setName("core-cpu-monitor"); + thread.setDaemon(true); + return thread; + }); + + // Rolling averages for system/process data + private static final RollingAverage SYSTEM_AVERAGE_10_SEC = new RollingAverage(10); + private static final RollingAverage SYSTEM_AVERAGE_1_MIN = new RollingAverage(60); + private static final RollingAverage SYSTEM_AVERAGE_15_MIN = new RollingAverage(60 * 15); + private static final RollingAverage PROCESS_AVERAGE_10_SEC = new RollingAverage(10); + private static final RollingAverage PROCESS_AVERAGE_1_MIN = new RollingAverage(60); + private static final RollingAverage PROCESS_AVERAGE_15_MIN = new RollingAverage(60 * 15); + + static { + try { + MBeanServer beanServer = ManagementFactory.getPlatformMBeanServer(); + ObjectName diagnosticBeanName = ObjectName.getInstance(OPERATING_SYSTEM_BEAN); + BEAN = JMX.newMXBeanProxy(beanServer, diagnosticBeanName, OperatingSystemMXBean.class); + } catch (Exception e) { + throw new UnsupportedOperationException("OperatingSystemMXBean is not supported by the system", e); + } + + // schedule rolling average calculations. + EXECUTOR.scheduleAtFixedRate(new RollingAverageCollectionTask(), 1, 1, TimeUnit.SECONDS); + } + + /** + * Ensures that the static initializer has been called. + */ + @SuppressWarnings("EmptyMethod") + public static void ensureMonitoring() { + // intentionally empty + } + + /** + * Returns the "recent cpu usage" for the whole system. This value is a + * double in the [0.0,1.0] interval. A value of 0.0 means that all CPUs + * were idle during the recent period of time observed, while a value + * of 1.0 means that all CPUs were actively running 100% of the time + * during the recent period being observed. All values betweens 0.0 and + * 1.0 are possible depending of the activities going on in the system. + * If the system recent cpu usage is not available, the method returns a + * negative value. + * + * @return the "recent cpu usage" for the whole system; a negative + * value if not available. + */ + public static double systemLoad() { + return BEAN.getSystemCpuLoad(); + } + + public static double systemLoad10SecAvg() { + return SYSTEM_AVERAGE_10_SEC.getAverage(); + } + + public static double systemLoad1MinAvg() { + return SYSTEM_AVERAGE_1_MIN.getAverage(); + } + + public static double systemLoad15MinAvg() { + return SYSTEM_AVERAGE_15_MIN.getAverage(); + } + + /** + * Returns the "recent cpu usage" for the Java Virtual Machine process. + * This value is a double in the [0.0,1.0] interval. A value of 0.0 means + * that none of the CPUs were running threads from the JVM process during + * the recent period of time observed, while a value of 1.0 means that all + * CPUs were actively running threads from the JVM 100% of the time + * during the recent period being observed. Threads from the JVM include + * the application threads as well as the JVM internal threads. All values + * betweens 0.0 and 1.0 are possible depending of the activities going on + * in the JVM process and the whole system. If the Java Virtual Machine + * recent CPU usage is not available, the method returns a negative value. + * + * @return the "recent cpu usage" for the Java Virtual Machine process; + * a negative value if not available. + */ + public static double processLoad() { + return BEAN.getProcessCpuLoad(); + } + + public static double processLoad10SecAvg() { + return PROCESS_AVERAGE_10_SEC.getAverage(); + } + + public static double processLoad1MinAvg() { + return PROCESS_AVERAGE_1_MIN.getAverage(); + } + + public static double processLoad15MinAvg() { + return PROCESS_AVERAGE_15_MIN.getAverage(); + } + + /** + * Task to poll CPU loads and add to the rolling averages in the enclosing class. + */ + private static final class RollingAverageCollectionTask implements Runnable { + private final RollingAverage[] systemAverages = new RollingAverage[]{ + SYSTEM_AVERAGE_10_SEC, + SYSTEM_AVERAGE_1_MIN, + SYSTEM_AVERAGE_15_MIN + }; + private final RollingAverage[] processAverages = new RollingAverage[]{ + PROCESS_AVERAGE_10_SEC, + PROCESS_AVERAGE_1_MIN, + PROCESS_AVERAGE_15_MIN + }; + + @Override + public void run() { + BigDecimal systemCpuLoad = new BigDecimal(systemLoad()); + BigDecimal processCpuLoad = new BigDecimal(processLoad()); + + if (systemCpuLoad.signum() != -1) { // if value is not negative + for (RollingAverage average : this.systemAverages) { + average.add(systemCpuLoad); + } + } + + if (processCpuLoad.signum() != -1) { // if value is not negative + for (RollingAverage average : this.processAverages) { + average.add(processCpuLoad); + } + } + } + } + + public interface OperatingSystemMXBean { + double getSystemCpuLoad(); + double getProcessCpuLoad(); + } +} \ No newline at end of file diff --git a/commons/src/main/java/zone/themcgamer/common/DoubleUtils.java b/commons/src/main/java/zone/themcgamer/common/DoubleUtils.java new file mode 100644 index 0000000..78fbc41 --- /dev/null +++ b/commons/src/main/java/zone/themcgamer/common/DoubleUtils.java @@ -0,0 +1,35 @@ +package zone.themcgamer.common; + +import java.util.Arrays; +import java.util.List; + +/** + * @author Braydon + */ +public class DoubleUtils { + /** + * Format the given value into a readable format + * @param amount the value to format + * @param shortSuffixes whether or not to have shorrt suffixes + * @return the formatted value + * @author Ell (modified by Braydon) + */ + public static String format(double amount, boolean shortSuffixes) { + if (amount <= 0.0D) + return "0"; + List suffixes; + if (shortSuffixes) + suffixes = Arrays.asList("", "k", "m", "b", "t", "Qa", "Qu", "Se", "Sp", "o", "n", "d"); + else + suffixes = Arrays.asList("", " Thousand", " Million", " Billion", " Trillion", " Quadrillion", + " Quintillion", " Sextillion", " Septillion", " Octillion", " Nonillion", " Decillion"); + double chunks = Math.floor(Math.floor(Math.log10(amount) / 3)); + amount/= Math.pow(10D, chunks * 3 - 1); + amount/= 10D; + String suffix = suffixes.get((int) chunks); + String format = MathUtils.formatString(amount, 1); + if (format.replace(".", "").length() > 5) + format = format.substring(0, 5); + return format + suffix; + } +} \ No newline at end of file diff --git a/commons/src/main/java/zone/themcgamer/common/EnumUtils.java b/commons/src/main/java/zone/themcgamer/common/EnumUtils.java new file mode 100644 index 0000000..69894d4 --- /dev/null +++ b/commons/src/main/java/zone/themcgamer/common/EnumUtils.java @@ -0,0 +1,26 @@ +package zone.themcgamer.common; + +import lombok.experimental.UtilityClass; +import org.jetbrains.annotations.Nullable; + +/** + * @author Braydon + */ +@UtilityClass +public class EnumUtils { + /** + * Get the enum value from the given class with the given name + * @param clazz - The enum class + * @param name - The name + * @return the enum value + */ + @Nullable + public static > T fromString(Class clazz, String name) { + for (T value : clazz.getEnumConstants()) { + if (value.name().equalsIgnoreCase(name)) { + return value; + } + } + return null; + } +} \ No newline at end of file diff --git a/commons/src/main/java/zone/themcgamer/common/HashUtils.java b/commons/src/main/java/zone/themcgamer/common/HashUtils.java new file mode 100644 index 0000000..5a195aa --- /dev/null +++ b/commons/src/main/java/zone/themcgamer/common/HashUtils.java @@ -0,0 +1,29 @@ +package zone.themcgamer.common; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * @author Braydon + */ +public class HashUtils { + /** + * Encrypt the given {@link String} as SHA-256 + * @param s the string to encrypt + * @return the encrypted string + */ + public static String encryptSha256(String s) { + try { + MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); + messageDigest.update(s.getBytes()); + byte digest[] = messageDigest.digest(); + StringBuffer buffer = new StringBuffer(); + for (byte b : digest) + buffer.append(Integer.toString((b & 0xff) + 0x100, 16).substring(1)); + return buffer.toString(); + } catch (NoSuchAlgorithmException ex) { + ex.printStackTrace(); + } + return null; + } +} \ No newline at end of file diff --git a/commons/src/main/java/zone/themcgamer/common/MathUtils.java b/commons/src/main/java/zone/themcgamer/common/MathUtils.java new file mode 100644 index 0000000..740942d --- /dev/null +++ b/commons/src/main/java/zone/themcgamer/common/MathUtils.java @@ -0,0 +1,27 @@ +package zone.themcgamer.common; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.Locale; + +/** + * @author Braydon + */ +public class MathUtils { + public static double round(double value, int places) { + if (places < 0) + throw new IllegalArgumentException(); + return new BigDecimal(value).setScale(places, RoundingMode.HALF_UP).doubleValue(); + } + + public static double format(double number, int additional) { + return Double.parseDouble(formatString(number, additional)); + } + + public static String formatString(double number, int additional) { + return new DecimalFormat("#.#" + "#".repeat(Math.max(0, additional - 1)), + new DecimalFormatSymbols(Locale.CANADA)).format(number); + } +} \ No newline at end of file diff --git a/commons/src/main/java/zone/themcgamer/common/MiscUtils.java b/commons/src/main/java/zone/themcgamer/common/MiscUtils.java new file mode 100644 index 0000000..2de6b06 --- /dev/null +++ b/commons/src/main/java/zone/themcgamer/common/MiscUtils.java @@ -0,0 +1,45 @@ +package zone.themcgamer.common; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +/** + * @author Braydon + */ +public class MiscUtils { + /** + * Get a {@link String} based on the provided string array + * @param array - The string array + * @return the string + */ + public static String arrayToString(String... array) { + return arrayToString(Arrays.asList(array)); + } + + /** + * Get a {@link String} based on the provided {@link List} + * @param list - The string list + * @return the string + */ + public static String arrayToString(List list) { + StringBuilder builder = new StringBuilder(); + for (String message : list) + builder.append(message).append("\n"); + return builder.substring(0, builder.toString().length() - 1); + } + + public static UUID getUuid(String s) { + if (s == null || (s.trim().isEmpty())) + return null; + try { + return UUID.fromString(s); + } catch (IllegalArgumentException ignored) {} + return null; + } + + public static String percent(double value, double max) { + double percent = (value * 100d) / max; + return (int) percent + "%"; + } +} \ No newline at end of file diff --git a/commons/src/main/java/zone/themcgamer/common/RandomUtils.java b/commons/src/main/java/zone/themcgamer/common/RandomUtils.java new file mode 100644 index 0000000..35bd9b2 --- /dev/null +++ b/commons/src/main/java/zone/themcgamer/common/RandomUtils.java @@ -0,0 +1,118 @@ +package zone.themcgamer.common; + +import lombok.experimental.UtilityClass; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +/** + * @author Braydon + */ +@UtilityClass +public class RandomUtils { + /** + * Return whether or not the {@param chance} has been met + * @param chance - The chance + * @param max - The maximum number + * @return whether or not the {@param chance} has been met + */ + public static boolean chance(int chance, int max) { + return randomInt(max) + 1 <= chance; + } + + /** + * Return whether or not the {@param chance} has been met + * @param chance - The chance + * @param max - The maximum number + * @return whether or not the {@param chance} has been met + */ + public static boolean chance(double chance, double max) { + return randomDouble(max) + 1 <= chance; + } + + /** + * Get a random int between 0 and the maximum value + * @param max - The maximum value + * @return the random number + */ + public static int randomInt(int max) { + return randomInt(0, max); + } + + /** + * Get a random int between the minimum and maximum values + * @param min - The minimum value + * @param max - The maximum value + * @return the random number + */ + public static int randomInt(int min, int max) { + return ThreadLocalRandom.current().nextInt(min, max); + } + + /** + * Get a random long between 0 and the maximum value + * @param max - The maximum value + * @return the random number + */ + public static long randomLong(long max) { + return randomLong(0L, max); + } + + /** + * Get a random long between the minimum and maximum values + * @param min - The minimum value + * @param max - The maximum value + * @return the random number + */ + public static long randomLong(long min, long max) { + return ThreadLocalRandom.current().nextLong(min, max); + } + + /** + * Get a random double between 0 and the maximum value + * @param max - The maximum value + * @return the random number + */ + public static double randomDouble(double max) { + return randomDouble(0D, max); + } + + /** + * Get a random double between the minimum and maximum values + * @param min - The minimum value + * @param max - The maximum value + * @return the random number + */ + public static double randomDouble(double min, double max) { + return ThreadLocalRandom.current().nextDouble(min, max); + } + + /** + * Select a random {@link Enum} value from the given + * {@link Enum} class + * @param enumClass - The enum class + * @return the random enum value + */ + @Nullable + public static > T random(Class enumClass) { + if (!enumClass.isEnum()) + throw new IllegalArgumentException("Class '" + enumClass.getSimpleName() + "' must be an enum"); + return random(Arrays.asList(enumClass.getEnumConstants())); + } + + /** + * Select a random {@link Object} from the provided {@link java.util.ArrayList} + * @param list - The list to get the object from + * @return the random object + */ + @Nullable + public static T random(List list) { + if (list.isEmpty()) + return null; + if (list.size() == 1) + return list.get(0); + return list.get(randomInt(0, list.size() - 1)); + } +} \ No newline at end of file diff --git a/commons/src/main/java/zone/themcgamer/common/RollingAverage.java b/commons/src/main/java/zone/themcgamer/common/RollingAverage.java new file mode 100644 index 0000000..a70969b --- /dev/null +++ b/commons/src/main/java/zone/themcgamer/common/RollingAverage.java @@ -0,0 +1,90 @@ +package zone.themcgamer.common; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; + +/** + * @author Lucko + * credits = https://github.com/lucko/spark + */ +public class RollingAverage { + + private final Queue samples; + private final int size; + private BigDecimal total = BigDecimal.ZERO; + + public RollingAverage(int size) { + this.size = size; + this.samples = new ArrayDeque<>(this.size); + } + + public void add(BigDecimal num) { + synchronized (this) { + this.total = this.total.add(num); + this.samples.add(num); + if (this.samples.size() > this.size) { + this.total = this.total.subtract(this.samples.remove()); + } + } + } + + public double getAverage() { + synchronized (this) { + if (this.samples.isEmpty()) { + return 0; + } + return this.total.divide(BigDecimal.valueOf(this.samples.size()), 30, RoundingMode.HALF_UP).doubleValue(); + } + } + + public double getMax() { + synchronized (this) { + BigDecimal max = BigDecimal.ZERO; + for (BigDecimal sample : this.samples) { + if (sample.compareTo(max) > 0) { + max = sample; + } + } + return max.doubleValue(); + } + } + + public double getMin() { + synchronized (this) { + BigDecimal min = BigDecimal.ZERO; + for (BigDecimal sample : this.samples) { + if (min == BigDecimal.ZERO || sample.compareTo(min) < 0) { + min = sample; + } + } + return min.doubleValue(); + } + } + + public double getMedian() { + return getPercentile(50); + } + + public double getPercentile(int percentile) { + if (percentile < 0 || percentile > 100) { + throw new IllegalArgumentException("Invalid percentage " + percentile); + } + + List sortedSamples; + synchronized (this) { + if (this.samples.isEmpty()) { + return 0; + } + sortedSamples = new ArrayList<>(this.samples); + } + sortedSamples.sort(null); + + int rank = (int) Math.ceil((percentile / 100d) * (sortedSamples.size() - 1)); + return sortedSamples.get(rank).doubleValue(); + } + +} \ No newline at end of file diff --git a/commons/src/main/java/zone/themcgamer/common/TimeUtils.java b/commons/src/main/java/zone/themcgamer/common/TimeUtils.java new file mode 100644 index 0000000..c72d125 --- /dev/null +++ b/commons/src/main/java/zone/themcgamer/common/TimeUtils.java @@ -0,0 +1,95 @@ +package zone.themcgamer.common; + +import java.text.SimpleDateFormat; + +/** + * @author Braydon + */ +public class TimeUtils { + /** + * Format the given time as a date and time {@link String} + * @param time the time to format + * @return the formatted time + */ + public static String when(long time) { + return new SimpleDateFormat("MM-dd-yyyy HH:mm:ss").format(time); + } + + public static String formatIntoDetailedString(long time, boolean shortTime) { + int secs = (int) (time / 1000L); + if (secs == 0) + return "0 " + (shortTime ? "s" : "seconds"); + int remainder = secs % 86400; + int days = secs / 86400; + int hours = remainder / 3600; + int minutes = remainder / 60 - hours * 60; + int seconds = remainder % 3600 - minutes * 60; + String fDays = (days > 0) ? (" " + days + (shortTime ? "d" : " day") + ((days > 1) ? shortTime ? "" : "s" : "")) : ""; + String fHours = (hours > 0) ? (" " + hours + (shortTime ? "h" : " hour") + ((hours > 1) ? shortTime ? "" : "s" : "")) : ""; + String fMinutes = (minutes > 0) ? (" " + minutes + (shortTime ? "m" : " minute") + ((minutes > 1) ? shortTime ? "" : "s" : "")) : ""; + String fSeconds = (seconds > 0) ? (" " + seconds + (shortTime ? "s" : " second") + ((seconds > 1) ? shortTime ? "" : "s" : "")) : ""; + return (fDays + fHours + fMinutes + fSeconds).trim(); + } + + /** + * Convert the provided unix time into readable time such as "1.0 Minute" + * @param time - The unix time to convert + * @return the formatted time + */ + public static String convertString(long time) { + return convertString(time, true, TimeUnit.FIT, false); + } + + /** + * Convert the provided unix time into readable time such as "1.0 Minute" + * @param time - The unix time to convert + * @param includeDecimals - Whether or not to format the time with decimals + * @param type - The type to format the time as. Use {@code TimeUnit.FIT} for + * the time to format based on the given unix time + * @param shortString - Whether or not the time string is shortened + * @return the formatted time + */ + public static String convertString(long time, boolean includeDecimals, TimeUnit type, boolean shortString) { + if (time == -1L) + return "Perm" + (shortString ? "" : "anent"); + else if (time <= 0L) + return "0.0" + (shortString ? "ms" : " Millisecond"); + if (type == TimeUnit.FIT) { + if (time < java.util.concurrent.TimeUnit.MINUTES.toMillis(1L)) + type = TimeUnit.SECONDS; + else if (time < java.util.concurrent.TimeUnit.HOURS.toMillis(1L)) + type = TimeUnit.MINUTES; + else if (time < java.util.concurrent.TimeUnit.DAYS.toMillis(1L)) + type = TimeUnit.HOURS; + else type = TimeUnit.DAYS; + } + double num; + String text; + if (type == TimeUnit.DAYS) { + num = MathUtils.format(time / 8.64E7, 1); + text = shortString ? "d" : " Day"; + } else if (type == TimeUnit.HOURS) { + num = MathUtils.format(time / 3600000.0, 1); + text = shortString ? "h" : " Hour"; + } else if (type == TimeUnit.MINUTES) { + num = MathUtils.format(time / 60000.0, 1); + text = shortString ? "m" : " Minute"; + } else if (type == TimeUnit.SECONDS) { + num = MathUtils.format(time / 1000.0, 1); + text = shortString ? "s" : " Second"; + } else { + num = MathUtils.format(time, 1); + text = shortString ? "ms" : " Millisecond"; + } + if (includeDecimals) + text = num + text; + else text = ((int) num) + text; + if (num != 1.0 && !shortString) + text+= "s"; + return text; + } + + public enum TimeUnit { + FIT, DAYS, HOURS, MINUTES, SECONDS, MILLISECONDS + } +} \ No newline at end of file diff --git a/commons/src/main/java/zone/themcgamer/common/TriTuple.java b/commons/src/main/java/zone/themcgamer/common/TriTuple.java new file mode 100644 index 0000000..f6002bc --- /dev/null +++ b/commons/src/main/java/zone/themcgamer/common/TriTuple.java @@ -0,0 +1,15 @@ +package zone.themcgamer.common; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +/** + * @author Braydon + */ +@AllArgsConstructor @Setter @Getter +public class TriTuple { + private A left; + private B middle; + private C right; +} \ No newline at end of file diff --git a/commons/src/main/java/zone/themcgamer/common/Tuple.java b/commons/src/main/java/zone/themcgamer/common/Tuple.java new file mode 100644 index 0000000..fc9d953 --- /dev/null +++ b/commons/src/main/java/zone/themcgamer/common/Tuple.java @@ -0,0 +1,24 @@ +package zone.themcgamer.common; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +/** + * @author Braydon + */ +@AllArgsConstructor @Setter @Getter +public class Tuple implements Cloneable { + private L left; + private R right; + + @Override + public Tuple clone() { + try { + return (Tuple) super.clone(); + } catch (CloneNotSupportedException ex) { + ex.printStackTrace(); + } + return null; + } +} \ No newline at end of file diff --git a/commons/src/main/java/zone/themcgamer/common/ZipUtils.java b/commons/src/main/java/zone/themcgamer/common/ZipUtils.java new file mode 100644 index 0000000..1b7b654 --- /dev/null +++ b/commons/src/main/java/zone/themcgamer/common/ZipUtils.java @@ -0,0 +1,70 @@ +package zone.themcgamer.common; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +/** + * @author Braydon + */ +public class ZipUtils { + /** + * Zip the given directory + * @param sourceDirectoryPath the path of the directory to zip + * @param zipDirectoryPath the path of the output file + */ + public static void zip(String sourceDirectoryPath, String zipDirectoryPath) { + try { + Path zipPath = Files.createFile(Paths.get(zipDirectoryPath)); + try (ZipOutputStream zipOutputStream = new ZipOutputStream(Files.newOutputStream(zipPath))) { + Path sourcePath = Paths.get(sourceDirectoryPath); + for (Path path : Files.walk(sourcePath).filter(path -> !Files.isDirectory(path)).collect(Collectors.toList())) { + ZipEntry zipEntry = new ZipEntry(sourcePath.relativize(path).toString()); + zipOutputStream.putNextEntry(zipEntry); + Files.copy(path, zipOutputStream); + zipOutputStream.closeEntry(); + } + } + } catch (IOException ex) { + ex.printStackTrace(); + } + } + + public static void unzip(File source, File output) throws IOException { + long started = System.currentTimeMillis(); + + FileInputStream fileInputStream = new FileInputStream(source); + ZipInputStream zipInputStream = new ZipInputStream(fileInputStream); + ZipEntry entry = zipInputStream.getNextEntry(); + while (entry != null) { + File file = new File(output, entry.getName()); + if (entry.isDirectory()) + file.mkdirs(); + else { + File parent = file.getParentFile(); + if (!parent.exists()) + parent.mkdirs(); + FileOutputStream fileOutputStream = new FileOutputStream(file); + BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream); + byte[] buffer = new byte[1024]; + int location; + while ((location = zipInputStream.read(buffer)) != -1) + bufferedOutputStream.write(buffer, 0, location); + bufferedOutputStream.close(); + fileOutputStream.close(); + } + entry = zipInputStream.getNextEntry(); + } + fileInputStream.close(); + + zipInputStream.closeEntry(); + zipInputStream.close(); + + System.out.println("Finished unzip process for \"" + source.getPath() + "\" in " + (System.currentTimeMillis() - started) + "ms"); + } +} \ No newline at end of file diff --git a/core/build.gradle.kts b/core/build.gradle.kts new file mode 100644 index 0000000..9204a3b --- /dev/null +++ b/core/build.gradle.kts @@ -0,0 +1,6 @@ +dependencies { + api(project(":serverdata")) + implementation("com.zaxxer:HikariCP:3.4.5") + compileOnly("com.destroystokyo:paperspigot:1.12.2") + implementation("com.github.cryptomorin:XSeries:7.8.0") +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/account/Account.java b/core/src/main/java/zone/themcgamer/core/account/Account.java new file mode 100644 index 0000000..044c247 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/account/Account.java @@ -0,0 +1,80 @@ +package zone.themcgamer.core.account; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import zone.themcgamer.data.Rank; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +/** + * @author Braydon + * @implNote The account implemention for {@link Account} + */ +@AllArgsConstructor @Setter @Getter @ToString +public class Account { + private final int id; + private final UUID uuid; + private final String name; + private Rank primaryRank; + private Rank[] secondaryRanks; + private final double gold, gems; + private final String lastEncryptedIpAddress, ipAddress, encryptedIpAddress; + private final long firstLogin, lastLogin; + + public String getPrimaryRankName() { + return primaryRank.getDisplayName(); + } + + public String[] getSecondaryRanksNames() { + String[] rankNames = new String[secondaryRanks.length]; + for (int i = 0; i < secondaryRanks.length; i++) + rankNames[i] = secondaryRanks[i].getDisplayName(); + return rankNames; + } + + public String getDisplayName() { + return primaryRank.getColor() + name; + } + + /** + * Check whether or not the player has the provided {@link Rank} + * @param rank the rank to check + * @return if the player has the rank + */ + public boolean hasRank(Rank rank) { + if (rank.ordinal() < primaryRank.ordinal()) // If the rank provided is above the player's rank, we return false + return false; + boolean checkSecondary = false; + // If the player's primary rank is a staff rank, and the rank to check is a donator rank, we skip over + // the primary rank checking and move onto the secondary rank checking + if ((primaryRank.getCategory() == Rank.RankCategory.STAFF && rank.getCategory() == Rank.RankCategory.DONATOR) + || rank.getCategory() == Rank.RankCategory.SUB) { + checkSecondary = true; + } + // If we aren't checking the secondary ranks, we check if the rank being checked is higher than the player's + // rank and return the value + if (!checkSecondary) + return rank.ordinal() >= primaryRank.ordinal(); + List secondaryRanks = Arrays.asList(this.secondaryRanks); + if (rank.getCategory() == Rank.RankCategory.DONATOR) { + int index = rank.ordinal(); + if (index > 0) { // If the rank index is above 0, we're able to fetch the previous rank in the list + Rank previousRank = Rank.values()[index - 1]; + // If the rank before the rank being checked is a donator rank, then we check if the player has + // the previous rank. If the player doesn't have the previous rank, but they have the rank being + // checked, we return true as they have the rank, otherwise return the hasPrevious value + if (previousRank.getCategory() == Rank.RankCategory.DONATOR) { + boolean hasPrevious = hasRank(previousRank); + if (!hasPrevious && secondaryRanks.contains(rank)) + return true; + return hasPrevious; + } + } + } + return secondaryRanks.contains(rank); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/account/AccountManager.java b/core/src/main/java/zone/themcgamer/core/account/AccountManager.java new file mode 100644 index 0000000..973961e --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/account/AccountManager.java @@ -0,0 +1,354 @@ +package zone.themcgamer.core.account; + +import com.cryptomorin.xseries.XSound; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.player.AsyncPlayerPreLoginEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerLoginEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.plugin.java.JavaPlugin; +import zone.themcgamer.common.EnumUtils; +import zone.themcgamer.core.account.command.GemsCommand; +import zone.themcgamer.core.account.command.GoldCommand; +import zone.themcgamer.core.account.command.PlayerInfoCommand; +import zone.themcgamer.core.account.command.rank.RankCommand; +import zone.themcgamer.core.account.command.rank.arguments.ClearArgument; +import zone.themcgamer.core.account.command.rank.arguments.InfoArgument; +import zone.themcgamer.core.account.command.rank.arguments.ListArgument; +import zone.themcgamer.core.account.command.rank.arguments.SetArgument; +import zone.themcgamer.core.account.event.AccountLoadEvent; +import zone.themcgamer.core.account.event.AccountUnloadEvent; +import zone.themcgamer.core.command.impl.social.MessageCommand; +import zone.themcgamer.core.command.impl.social.ReplyCommand; +import zone.themcgamer.core.common.MojangUtils; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.core.module.Module; +import zone.themcgamer.core.module.ModuleInfo; +import zone.themcgamer.core.nametag.NametagManager; +import zone.themcgamer.data.Rank; +import zone.themcgamer.data.jedis.cache.CacheRepository; +import zone.themcgamer.data.jedis.cache.impl.PlayerCache; +import zone.themcgamer.data.jedis.command.JedisCommandHandler; +import zone.themcgamer.data.jedis.command.impl.RankMessageCommand; +import zone.themcgamer.data.jedis.command.impl.StaffChatCommand; +import zone.themcgamer.data.jedis.command.impl.account.AccountRankClearCommand; +import zone.themcgamer.data.jedis.command.impl.account.AccountRankSetCommand; +import zone.themcgamer.data.jedis.command.impl.player.PlayerDirectMessageEvent; +import zone.themcgamer.data.jedis.repository.RedisRepository; +import zone.themcgamer.data.mysql.MySQLController; + +import java.sql.SQLException; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +/** + * @author Braydon + */ +@ModuleInfo(name = "Account Manager") +public class AccountManager extends Module { + public static final List> MINI_ACCOUNTS = new ArrayList<>(); + public static final Map CACHE = new HashMap<>(); // Account cache for online players + public static final Cache LOOKUP_CACHE = CacheBuilder.newBuilder() // Account cache for players that were looked up via the lookup method + .expireAfterWrite(10, TimeUnit.MINUTES) + .removalListener(removalNotification -> { + Object key = removalNotification.getKey(); + if (key instanceof UUID) { + if (CACHE.containsKey(key)) + return; + for (MiniAccount miniAccount : MINI_ACCOUNTS) + miniAccount.getAccounts().remove(key); + } + }).build(); + + private static final String KICK_MESSAGE = "Failed to fetch your account data, please try again in a few moments"; + + private final AccountRepository repository; + private final CacheRepository cacheRepository; + private final NametagManager nametagManager; + + private final AtomicInteger playersConnecting = new AtomicInteger(); // The amount of players connecting to the server + private final AtomicInteger accountsLoading = new AtomicInteger(); // The amount of players connecting to the server + private final List loggingIn = Collections.synchronizedList(new ArrayList<>()); // The list of uuids logging in + + public AccountManager(JavaPlugin plugin, MySQLController mySQLController, NametagManager nametagManager) { + super(plugin); + repository = new AccountRepository(mySQLController.getDataSource()); + cacheRepository = RedisRepository.getRepository(CacheRepository.class).orElse(null); + this.nametagManager = nametagManager; + + // In-case somebody decides to do a no no and reloads the server, we wanna kick all + // online players so they can rejoin to load their account + for (Player player : Bukkit.getOnlinePlayers()) + Bukkit.getScheduler().runTask(plugin, () -> player.kickPlayer("Please re-join")); + + registerCommand(new RankCommand()); + registerCommand(new InfoArgument(this)); + registerCommand(new SetArgument(this)); + registerCommand(new ClearArgument(this)); + registerCommand(new ListArgument()); + + registerCommand(new GoldCommand(this)); + registerCommand(new GemsCommand(this)); + registerCommand(new MessageCommand(this, cacheRepository)); + registerCommand(new ReplyCommand(this, cacheRepository)); + registerCommand(new PlayerInfoCommand(this, cacheRepository)); + registerCommand(new zone.themcgamer.core.command.impl.staff.StaffChatCommand()); + + JedisCommandHandler.getInstance().addListener(jedisCommand -> { + if (jedisCommand instanceof AccountRankSetCommand) { + AccountRankSetCommand accountRankSetCommand = (AccountRankSetCommand) jedisCommand; + Player player = Bukkit.getPlayer(accountRankSetCommand.getUuid()); + if (player != null) { + Rank rank = EnumUtils.fromString(Rank.class, accountRankSetCommand.getConstantName()); + if (rank == null) + rank = Rank.DEFAULT; + Rank finalRank = rank; + fromCache(player.getUniqueId()).ifPresent(account -> account.setPrimaryRank(finalRank)); + nametagManager.setNametag(player, rank.getNametag(), null, rank.ordinal() + 1); + player.sendMessage(Style.main("Rank", "Your rank was updated to §f" + accountRankSetCommand.getRankDisplayName())); + } + } else if (jedisCommand instanceof AccountRankClearCommand) { + AccountRankClearCommand accountRankClearCommand = (AccountRankClearCommand) jedisCommand; + Player player = Bukkit.getPlayer(accountRankClearCommand.getUuid()); + if (player != null) { + fromCache(player.getUniqueId()).ifPresent(account -> { + account.setPrimaryRank(Rank.DEFAULT); + account.setSecondaryRanks(new Rank[0]); + + Rank rank = account.getPrimaryRank(); + nametagManager.setNametag(player, rank.getNametag(), null, rank.ordinal() + 1); + }); + player.sendMessage(Style.main("Rank", "Your ranks were cleared")); + } + } else if (jedisCommand instanceof RankMessageCommand) { + RankMessageCommand rankMessageCommand = (RankMessageCommand) jedisCommand; + for (Player player : Bukkit.getOnlinePlayers()) { + Optional optionalAccount = fromCache(player.getUniqueId()); + if (optionalAccount.isEmpty()) + continue; + if (optionalAccount.get().hasRank(rankMessageCommand.getRank())) + player.sendMessage(rankMessageCommand.getMessage()); + } + } else if (jedisCommand instanceof StaffChatCommand) { + StaffChatCommand staffChatCommand = (StaffChatCommand) jedisCommand; + for (Player player : Bukkit.getOnlinePlayers()) { + Optional account = fromCache(player.getUniqueId()); + if (account.isEmpty() || (!account.get().hasRank(Rank.HELPER))) + continue; + String format = staffChatCommand.getPrefix() + + " &7" + staffChatCommand.getUsername() + " &8» &f" + staffChatCommand.getMessage(); + player.sendMessage(Style.main(staffChatCommand.getServer(), format)); + } + } else if (jedisCommand instanceof PlayerDirectMessageEvent) { + PlayerDirectMessageEvent playerDirectMessageEvent = (PlayerDirectMessageEvent) jedisCommand; + UUID uuid = playerDirectMessageEvent.getReceiver(); + Player player = Bukkit.getPlayer(uuid); + if (player == null || !player.isOnline()) + return; + + player.sendMessage(Style.color("&b\u2709 &7(from " + playerDirectMessageEvent.getSenderDisplayName() + "&7) &8\u00BB&f " + playerDirectMessageEvent.getMessage())); + player.playSound(player.getLocation(), XSound.ENTITY_CHICKEN_EGG.parseSound(), 10, 1); + } + }); + } + + @EventHandler(priority = EventPriority.LOWEST) + private void onAsyncPreLogin(AsyncPlayerPreLoginEvent event) { + try { + UUID uuid = event.getUniqueId(); + + // Incrementing the players connecting + playersConnecting.incrementAndGet(); + + // If the amount of accounts loading is 3 or more, we wanna sleep the thread (delay the login) for + // 25 milliseconds. This will cause less strain on the MySQL server with multiple queries at once. + while (accountsLoading.get() >= 3) { + try { + Thread.sleep(25L); + } catch (InterruptedException ex) { + ex.printStackTrace(); + } + } + + try { + // We store the time the player started connecting and add them to the logging in list + long started = System.currentTimeMillis(); + accountsLoading.incrementAndGet(); + loggingIn.add(uuid); + Account[] accountArray = new Account[] { null }; + AtomicBoolean repositoryException = new AtomicBoolean(); + + // Loading the players account from MySQL and removing them from the logging in list + Bukkit.getScheduler().runTaskAsynchronously(getPlugin(), () -> { + try { + accountArray[0] = repository.login(uuid, event.getName(), event.getAddress().getHostAddress()); + } catch (SQLException ex) { + repositoryException.set(true); + ex.printStackTrace(); + } + loggingIn.remove(uuid); + }); + // We sleep the thread for however long it takes the player's account to be fetched from + // the database (with a maximum time of 20 seconds) to give the MySQL server time to fetch + // the account + long timeSlept = 0L; + while (loggingIn.contains(uuid) && System.currentTimeMillis() - started < TimeUnit.SECONDS.toMillis(20L)) { + if (repositoryException.get()) { + event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, KICK_MESSAGE + " (repository)"); + break; + } + timeSlept+= 1L; + Thread.sleep(1L); + } + log(event.getName() + " has taken " + (System.currentTimeMillis() - started) + "ms to login (" + timeSlept + "ms was spent sleeping)"); + + Account account = accountArray[0]; + // If the player is still in the logging in list, or the player's account is null, we wanna disallow login + // and show the player an error + if (loggingIn.remove(uuid) || account == null || (account.getId() <= 0)) + event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, KICK_MESSAGE + " (" + (account == null ? "account" : "timeout") + ")"); + else { + // If the login was successful, we call the AccountLoadEvent and locally cache the player's account + // so it can be used in the future + Bukkit.getScheduler().runTask(getPlugin(), () -> Bukkit.getPluginManager().callEvent(new AccountLoadEvent(account))); + CACHE.put(uuid, account); + } + } catch (Exception ex) { + event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, KICK_MESSAGE + " (exception)"); + ex.printStackTrace(); + } finally { + accountsLoading.decrementAndGet(); + } + } finally { + playersConnecting.decrementAndGet(); + } + } + + @EventHandler(priority = EventPriority.LOWEST) + private void onLogin(PlayerLoginEvent event) { + Player player = event.getPlayer(); + Account account; + // If the player does not have an account (this should NEVER happen), disallow login + if ((account = CACHE.get(player.getUniqueId())) == null) + event.disallow(PlayerLoginEvent.Result.KICK_OTHER, KICK_MESSAGE + " (cache)"); + else { + // Automatic opping for player's with the rank Jr.Dev or above + Rank opRank = Rank.JR_DEVELOPER; + if (account.hasRank(opRank) && !player.isOp()) + player.setOp(true); + else if (!account.hasRank(opRank) && player.isOp()) + player.setOp(false); + } + } + + @EventHandler(priority = EventPriority.LOWEST) + private void onJoin(PlayerJoinEvent event) { + Player player = event.getPlayer(); + Account account = CACHE.get(player.getUniqueId()); + if (account == null) + return; + Rank rank = account.getPrimaryRank(); + nametagManager.setNametag(player, rank.getNametag(), null, rank.ordinal() + 1); + } + + @EventHandler(priority = EventPriority.HIGHEST) + private void onQuit(PlayerQuitEvent event) { + UUID uuid = event.getPlayer().getUniqueId(); + // Loop through all of the mini accounts and remove the player's account + for (MiniAccount miniAccount : MINI_ACCOUNTS) + miniAccount.getAccounts().remove(uuid); + // Call the unload event and remove the main account + Bukkit.getPluginManager().callEvent(new AccountUnloadEvent(CACHE.remove(uuid))); + } + + /** + * Lookup a {@link Account} with the given username + * @param name the name of the account to lookup + * @param consumer the account consumer + */ + public void lookup(String name, Consumer consumer) { + CompletableFuture.runAsync(() -> { + Player player = Bukkit.getPlayerExact(name); + UUID uuid = player == null ? null : player.getUniqueId(); + if (uuid == null) { + Optional optionalPlayerCache = cacheRepository + .lookup(PlayerCache.class, playerCache -> playerCache.getName().equalsIgnoreCase(name)); + if (optionalPlayerCache.isPresent()) + uuid = optionalPlayerCache.get().getUuid(); + } + if (uuid == null) + MojangUtils.getUUIDAsync(name, fetchedUUID -> lookup(fetchedUUID, name, consumer)); + else lookup(uuid, name, consumer); + }); + } + + /** + * Lookup a {@link Account} with the given {@link UUID} + * @param uuid the uuid of the account to lookup + * @param name the name of the account to lookup + * @param consumer the account consumer + */ + public void lookup(UUID uuid, String name, Consumer consumer) { + if (uuid == null) { + consumer.accept(null); + return; + } + AtomicReference reference = new AtomicReference<>(CACHE.get(uuid)); + if (reference.get() == null) + reference.set(LOOKUP_CACHE.getIfPresent(uuid)); + if (reference.get() != null) + consumer.accept(reference.get()); + else { + CompletableFuture.runAsync(() -> { + try { + reference.set(repository.login(uuid, name, "")); + if (reference.get() != null) + LOOKUP_CACHE.put(uuid, reference.get()); + } catch (SQLException ex) { + ex.printStackTrace(); + } + consumer.accept(reference.get()); + }); + } + } + + public void setRank(Account account, Rank rank) { + account.setPrimaryRank(rank); + repository.setRank(account.getId(), rank); + JedisCommandHandler.getInstance().send(new AccountRankSetCommand(account.getUuid(), rank.name(), rank.getColor() + rank.getDisplayName())); + } + + public void clearRanks(Account account) { + account.setPrimaryRank(Rank.DEFAULT); + account.setSecondaryRanks(new Rank[0]); + repository.clearRanks(account.getId()); + JedisCommandHandler.getInstance().send(new AccountRankClearCommand(account.getUuid())); + } + + /** + * Add the given {@link MiniAccount} + * @param miniAccount the mini account to add + */ + public static void addMiniAccount(MiniAccount miniAccount) { + MINI_ACCOUNTS.add(miniAccount); + } + + /** + * Get the {@link Account} from the provided uuid + * @param uuid the uuid to get the account for + * @return the optional account + */ + public static Optional fromCache(UUID uuid) { + return Optional.ofNullable(CACHE.get(uuid)); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/account/AccountRepository.java b/core/src/main/java/zone/themcgamer/core/account/AccountRepository.java new file mode 100644 index 0000000..5e46ef2 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/account/AccountRepository.java @@ -0,0 +1,208 @@ +package zone.themcgamer.core.account; + +import com.zaxxer.hikari.HikariDataSource; +import org.bukkit.Bukkit; +import zone.themcgamer.common.HashUtils; +import zone.themcgamer.core.account.event.AccountPreLoadEvent; +import zone.themcgamer.data.Rank; +import zone.themcgamer.data.jedis.cache.CacheRepository; +import zone.themcgamer.data.jedis.cache.impl.PlayerCache; +import zone.themcgamer.data.jedis.repository.RedisRepository; +import zone.themcgamer.data.mysql.data.column.Column; +import zone.themcgamer.data.mysql.data.column.impl.IntegerColumn; +import zone.themcgamer.data.mysql.data.column.impl.LongColumn; +import zone.themcgamer.data.mysql.data.column.impl.StringColumn; +import zone.themcgamer.data.mysql.repository.MySQLRepository; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +/** + * @author Braydon + */ +public class AccountRepository extends MySQLRepository { + private static final String SELECT_ACCOUNT = "SELECT * FROM `accounts` WHERE `uuid` = ? LIMIT 1"; + private static final String INSERT_ACCOUNT = "INSERT INTO `accounts` " + + "(`id`, `uuid`, `name`, `primaryRank`, `secondaryRanks`, `gold`, `gems`, `ipAddress`, `firstLogin`, `lastLogin`) VALUES " + + "(NULL, ?, ?, '" + Rank.DEFAULT.name() + "', '', '0', '0', ?, ?, ?);"; + private static final String UPDATE_RANK = "UPDATE `accounts` SET `primaryRank` = ? WHERE `id` = ?;"; + + public AccountRepository(HikariDataSource dataSource) { + super(dataSource); + } + + /** + * Attempt a login with the given uuid, name, and ip address + * @param uuid the uuid + * @param name the username + * @param ipAddress the ip address + * @return the fetched account + */ + public Account login(UUID uuid, String name, String ipAddress) throws SQLException { + if (uuid == null || (name == null || name.trim().isEmpty())) + return null; + boolean offlineLookup = ipAddress.trim().isEmpty(); + String encryptedIpAddress = offlineLookup ? "" : HashUtils.encryptSha256(ipAddress); + int accountId = -1; + boolean loadedFromCache = false; + CacheRepository cacheRepository = RedisRepository.getRepository(CacheRepository.class).orElse(null); + if (cacheRepository != null) { + PlayerCache cache = cacheRepository.lookup(PlayerCache.class, uuid).orElse(null); + if (cache != null) { + if (!cache.getName().equals(name)) { + cache.setName(name); + cacheRepository.post(cache); + } + accountId = cache.getAccountId(); + loadedFromCache = true; + } + } + Account account = null; + long now = System.currentTimeMillis(); + try ( + Connection connection = dataSource.getConnection(); + PreparedStatement statement = connection.prepareStatement(SELECT_ACCOUNT); + ) { + // Attempt to select the existing account from the database + statement.setString(1, uuid.toString()); + ResultSet resultSet = statement.executeQuery(); + + String query = ""; + + if (resultSet.next()) { // If the account exists in the database, we wanna load its values + if (accountId <= 0) // If the account id has not been loaded from the cache, we wanna fetch it from the database + accountId = resultSet.getInt(1); + account = constructAccount(accountId, uuid, name, resultSet, ipAddress, encryptedIpAddress, offlineLookup ? -1L : now); + + // If the account exists in the database and we're not doing an offline account lookup, we wanna update + // some key values in the database for the user, like their name, ip address, and last login time + if (!offlineLookup) + query = "UPDATE `accounts` SET `name`='" + name + "', `ipAddress`='" + encryptedIpAddress + "', `lastLogin`='" + System.currentTimeMillis() + "' WHERE `id` = '" + accountId + "';"; + } else { + // Inserting the new account into the database + int[] idArray = new int[1]; + executeInsert(connection, INSERT_ACCOUNT, new Column[] { + new StringColumn("uuid", uuid.toString()), + new StringColumn("name", name), + new StringColumn("ipAddress", encryptedIpAddress), + new LongColumn("firstLogin", now), + new LongColumn("lastLogin", now), + }, insertResultSet -> { + try { + while (insertResultSet.next()) { + // After we insert the account, we wanna get the account id that was generated and + // store it in the account object + idArray[0] = insertResultSet.getInt(1); + } + } catch (SQLException ex) { + ex.printStackTrace(); + } + }); + accountId = idArray[0]; + } + Bukkit.getPluginManager().callEvent(new AccountPreLoadEvent(uuid, name, ipAddress)); + int finalAccountId = accountId; + query+= AccountManager.MINI_ACCOUNTS.parallelStream().map(miniAccount -> miniAccount.getQuery(finalAccountId, uuid, name, ipAddress, encryptedIpAddress)).collect(Collectors.joining()); + if (!query.trim().isEmpty()) { + statement.execute(query); + statement.getUpdateCount(); + statement.getMoreResults(); + for (MiniAccount miniAccount : AccountManager.MINI_ACCOUNTS) { + Object miniAccountObject = miniAccount.getAccount(accountId, uuid, name, ipAddress, encryptedIpAddress); + if (miniAccountObject != null) + miniAccount.addAccount(uuid, miniAccountObject); + try { + ResultSet miniAccountResultSet = statement.getResultSet(); + if (miniAccountResultSet == null) + continue; + miniAccount.loadAccount(accountId, uuid, name, ipAddress, encryptedIpAddress, miniAccountResultSet); + } finally { + statement.getMoreResults(); + } + } + } + } + if (account == null) { + account = new Account( + accountId, + uuid, + name, + Rank.DEFAULT, + new Rank[0], + 0D, + 0D, + encryptedIpAddress, + ipAddress, + encryptedIpAddress, + now, + now + ); + } + if (!loadedFromCache && cacheRepository != null) + cacheRepository.post(new PlayerCache(uuid, name, accountId)); + return account; + } + + public void setRank(int accountId, Rank rank) { + CompletableFuture.runAsync(() -> { + executeInsert(UPDATE_RANK, new Column[] { + new StringColumn("primaryRank", rank.name()), + new IntegerColumn("id", accountId) + }); + }); + } + + public void clearRanks(int accountId) { + String query = UPDATE_RANK + .replaceFirst("\\?", "'" + Rank.DEFAULT.name() + "'") + .replaceFirst("\\?", "'" + accountId + "'"); + query+= "UPDATE `accounts` SET `secondaryRanks` = '' WHERE `id` = " + accountId + ";"; + String finalQuery = query; + CompletableFuture.runAsync(() -> { + executeInsert(finalQuery, new Column[0]); + }); + } + + /** + * Construct a {@link Account} from the given parameters + * @param accountId the account id + * @param uuid the uuid + * @param name the name + * @param resultSet the result set + * @param ipAddress the ip address + * @param encryptedIpAddress the encrypted ip address + * @param lastLogin the last login + * @return the account + */ + private Account constructAccount(int accountId, UUID uuid, String name, ResultSet resultSet, String ipAddress, String encryptedIpAddress, long lastLogin) { + try { + Rank[] secondaryRanks = Arrays.stream(resultSet.getString("secondaryRanks") + .split(",")).map(rankName -> Rank.lookup(rankName).orElse(null)) + .filter(Objects::nonNull).toArray(Rank[]::new); + return new Account( + accountId, + uuid, + resultSet.getString("name"), + Rank.lookup(resultSet.getString("primaryRank")).orElse(Rank.DEFAULT), + secondaryRanks, + resultSet.getInt("gold"), + resultSet.getInt("gems"), + resultSet.getString("ipAddress"), + ipAddress, + encryptedIpAddress, + resultSet.getLong("firstLogin"), + lastLogin == -1L ? resultSet.getLong("lastLogin") : lastLogin + ); + } catch (SQLException ex) { + ex.printStackTrace(); + } + return null; + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/account/MiniAccount.java b/core/src/main/java/zone/themcgamer/core/account/MiniAccount.java new file mode 100644 index 0000000..514e182 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/account/MiniAccount.java @@ -0,0 +1,83 @@ +package zone.themcgamer.core.account; + +import lombok.Getter; +import org.bukkit.plugin.java.JavaPlugin; +import zone.themcgamer.core.module.Module; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.function.Predicate; + +/** + * @author Braydon + * @implNote A mini account makes it easier to organize accounts into different sections. + * For instance, important information regarding a player is stored in + * {@link Account}, and other things will be stored in a mini account. + */ +@Getter +public abstract class MiniAccount extends Module { + private final Map accounts = new HashMap<>(); + + public MiniAccount(JavaPlugin plugin) { + super(plugin); + } + + /** + * Get the default account using the given account id, uuid, or name + * @param accountId the account id + * @param uuid the uuid + * @param name the name + * @return the default account + */ + public abstract T getAccount(int accountId, UUID uuid, String name, String ip, String encryptedIp); + + /** + * Get the query to fetch the account using the given account id, uuid, or name + * @param accountId the account id + * @param uuid the uuid + * @param name the name + * @return the query + */ + public abstract String getQuery(int accountId, UUID uuid, String name, String ip, String encryptedIp); + + /** + * Called when an account is fetched from MySQL + * @param accountId the account id + * @param uuid the uuid + * @param name the name + * @param resultSet the result set that was fetched + * @throws SQLException exception + */ + public abstract void loadAccount(int accountId, UUID uuid, String name, String ip, String encryptedIp, ResultSet resultSet) throws SQLException; + + /** + * Add the provided account identified by the given {@link UUID} + * @param uuid the uuid to identify the account + * @param object the account + */ + public void addAccount(UUID uuid, Object object) { + accounts.put(uuid, (T) object); + } + + /** + * Get the {@link T} account for the given {@link UUID} + * @param uuid the uuid of the account + * @return the optional account + */ + public Optional lookup(UUID uuid) { + return Optional.ofNullable(accounts.get(uuid)); + } + + /** + * Get the {@link T} account that tests against the {@link Predicate} + * @param predicate the predicate to test against + * @return the optional account + */ + public Optional lookup(Predicate predicate) { + return accounts.values().stream().filter(predicate).findFirst(); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/account/command/GemsCommand.java b/core/src/main/java/zone/themcgamer/core/account/command/GemsCommand.java new file mode 100644 index 0000000..35ccdb4 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/account/command/GemsCommand.java @@ -0,0 +1,34 @@ +package zone.themcgamer.core.account.command; + +import lombok.AllArgsConstructor; +import org.bukkit.entity.Player; +import zone.themcgamer.common.DoubleUtils; +import zone.themcgamer.core.account.AccountManager; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.core.common.Style; + +@AllArgsConstructor +public class GemsCommand { + private final AccountManager accountManager; + + @Command(name = "gems", description = "View your gems", playersOnly = true) + public void onCommand(CommandProvider command) { + Player player = command.getPlayer(); + String[] args = command.getArgs(); + String target = player.getName(); + if (args.length > 0) + target = args[0]; + String finalTarget = target; + accountManager.lookup(target, account -> { + if (account == null) { + player.sendMessage(Style.invalidAccount("Account", finalTarget)); + return; + } + String gems = DoubleUtils.format(account.getGems(), false); + if (player.getName().equals(account.getName())) + player.sendMessage(Style.main("Account", String.format("You have &2%s Gem" + (account.getGems() == 1 ? "" : "s") + "&7!", gems))); + else player.sendMessage(Style.main("Account", String.format("&b%s &7has &2%s Gem" + (account.getGems() == 1 ? "" : "s") + "&7!", account.getDisplayName(), gems))); + }); + } +} diff --git a/core/src/main/java/zone/themcgamer/core/account/command/GoldCommand.java b/core/src/main/java/zone/themcgamer/core/account/command/GoldCommand.java new file mode 100644 index 0000000..20a0fd8 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/account/command/GoldCommand.java @@ -0,0 +1,34 @@ +package zone.themcgamer.core.account.command; + +import lombok.AllArgsConstructor; +import org.bukkit.entity.Player; +import zone.themcgamer.common.DoubleUtils; +import zone.themcgamer.core.account.AccountManager; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.core.common.Style; + +@AllArgsConstructor +public class GoldCommand { + private final AccountManager accountManager; + + @Command(name = "gold", description = "View your gold", playersOnly = true) + public void onCommand(CommandProvider command) { + Player player = command.getPlayer(); + String[] args = command.getArgs(); + String target = player.getName(); + if (args.length > 0) + target = args[0]; + String finalTarget = target; + accountManager.lookup(target, account -> { + if (account == null) { + player.sendMessage(Style.invalidAccount("Account", finalTarget)); + return; + } + String gold = DoubleUtils.format(account.getGold(), false); + if (player.getName().equals(account.getName())) + player.sendMessage(Style.main("Account", String.format("You have &6%s Gold&7!", gold))); + else player.sendMessage(Style.main("Account", String.format("&b%s &7has &6%s Gold&7!", account.getDisplayName(), gold))); + }); + } +} diff --git a/core/src/main/java/zone/themcgamer/core/account/command/PlayerInfoCommand.java b/core/src/main/java/zone/themcgamer/core/account/command/PlayerInfoCommand.java new file mode 100644 index 0000000..abc122a --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/account/command/PlayerInfoCommand.java @@ -0,0 +1,51 @@ +package zone.themcgamer.core.account.command; + +import lombok.AllArgsConstructor; +import org.bukkit.command.CommandSender; +import zone.themcgamer.common.TimeUtils; +import zone.themcgamer.core.account.AccountManager; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.data.Rank; +import zone.themcgamer.data.jedis.cache.CacheRepository; +import zone.themcgamer.data.jedis.cache.impl.PlayerStatusCache; + +import java.util.Optional; + +@AllArgsConstructor +public class PlayerInfoCommand { + private final AccountManager accountManager; + private final CacheRepository cacheRepository; + + @Command(name = "playerinfo", aliases = { "pinfo" }, description = "Get information about a player", ranks = { Rank.HELPER }) + public void onCommand(CommandProvider command) { + CommandSender sender = command.getSender(); + String[] args = command.getArgs(); + if (args.length < 1) { + sender.sendMessage(Style.main("Account", "Usage: /" + command.getLabel() + " ")); + return; + } + accountManager.lookup(args[0], account -> { + if (account == null) { + sender.sendMessage(Style.invalidAccount("Account", args[0])); + return; + } + + Optional playerStatusCacheOptional = cacheRepository.lookup(PlayerStatusCache.class, account.getUuid()); + sender.sendMessage(""); + sender.sendMessage(Style.color("&a&lPlayer Information")); + sender.sendMessage(Style.color("&7Id: &c" + account.getId())); + sender.sendMessage(Style.color("&7Player: &b" + account.getName())); + sender.sendMessage(Style.color("&7Status: " + (playerStatusCacheOptional.isEmpty() ? "&cOffline" : "&aOnline"))); + sender.sendMessage(Style.color("&7Server: &b" + (playerStatusCacheOptional.isEmpty() ? "&cN/A" : playerStatusCacheOptional.get().getServer()))); + sender.sendMessage(Style.color("&7Registered At: &b" + TimeUtils.when(account.getFirstLogin()))); + sender.sendMessage(Style.color("&7Last Seen: &b" + TimeUtils.when(account.getLastLogin()))); + sender.sendMessage(Style.color("&7Rank: &b" + account.getPrimaryRank().getColor() + account.getPrimaryRank().getDisplayName())); + sender.sendMessage(Style.color("&7Sub Ranks: &b" + (account.getSecondaryRanksNames().length == 0 ? "None" : String.join("§7, §f", account.getSecondaryRanksNames())))); + sender.sendMessage(Style.color("&7Gold: &b" + account.getGold())); + sender.sendMessage(Style.color("&7Gems: &b" + account.getGems())); + sender.sendMessage(""); + }); + } +} diff --git a/core/src/main/java/zone/themcgamer/core/account/command/rank/RankCommand.java b/core/src/main/java/zone/themcgamer/core/account/command/rank/RankCommand.java new file mode 100644 index 0000000..d90d037 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/account/command/rank/RankCommand.java @@ -0,0 +1,14 @@ +package zone.themcgamer.core.account.command.rank; + +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.core.command.help.HelpCommand; +import zone.themcgamer.data.Rank; + +/** + * @author Braydon + */ +public class RankCommand extends HelpCommand { + @Command(name = "rank", description = "Rank management", ranks = { Rank.ADMIN }) + public void onCommand(CommandProvider command) {} +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/account/command/rank/arguments/ClearArgument.java b/core/src/main/java/zone/themcgamer/core/account/command/rank/arguments/ClearArgument.java new file mode 100644 index 0000000..f803a76 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/account/command/rank/arguments/ClearArgument.java @@ -0,0 +1,36 @@ +package zone.themcgamer.core.account.command.rank.arguments; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.bukkit.command.CommandSender; +import zone.themcgamer.core.account.AccountManager; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.data.Rank; + +/** + * @author Braydon + */ +@AllArgsConstructor @Getter +public class ClearArgument { + private final AccountManager accountManager; + + @Command(name = "rank.clear", usage = "", description = "Clear the ranks for a player", ranks = { Rank.ADMIN }) + public void onCommand(CommandProvider command) { + CommandSender sender = command.getSender(); + String[] args = command.getArgs(); + if (args.length < 1) { + sender.sendMessage(Style.main("Rank", "Usage: /rank clear ")); + return; + } + accountManager.lookup(args[0], account -> { + if (account == null) { + sender.sendMessage(Style.invalidAccount("Rank", args[0])); + return; + } + sender.sendMessage(Style.main("Rank", account.getDisplayName() + " §7had their ranks cleared")); + accountManager.clearRanks(account); + }); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/account/command/rank/arguments/InfoArgument.java b/core/src/main/java/zone/themcgamer/core/account/command/rank/arguments/InfoArgument.java new file mode 100644 index 0000000..63418b9 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/account/command/rank/arguments/InfoArgument.java @@ -0,0 +1,41 @@ +package zone.themcgamer.core.account.command.rank.arguments; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.bukkit.command.CommandSender; +import zone.themcgamer.core.account.AccountManager; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.data.Rank; + +import java.util.Arrays; +import java.util.stream.Collectors; + +/** + * @author Braydon + */ +@AllArgsConstructor @Getter +public class InfoArgument { + private final AccountManager accountManager; + + @Command(name = "rank.info", usage = "", description = "View rank info for a player", ranks = { Rank.MODERATOR }) + public void onCommand(CommandProvider command) { + CommandSender sender = command.getSender(); + String[] args = command.getArgs(); + if (args.length < 1) { + sender.sendMessage(Style.main("Rank", "Usage: /rank info ")); + return; + } + accountManager.lookup(args[0], account -> { + if (account == null) { + sender.sendMessage(Style.invalidAccount("Rank", args[0])); + return; + } + sender.sendMessage(Style.main("Rank", "Rank information for " + account.getDisplayName() + "§7:")); + sender.sendMessage(" §8- §7Primary Rank §f" + account.getPrimaryRank().getDisplayName()); + sender.sendMessage(" §8- §7Sub Ranks §f" + (account.getSecondaryRanks().length < 1 ? "None" : + Arrays.stream(account.getSecondaryRanks()).map(Rank::getDisplayName).collect(Collectors.joining("§7, §f")))); + }); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/account/command/rank/arguments/ListArgument.java b/core/src/main/java/zone/themcgamer/core/account/command/rank/arguments/ListArgument.java new file mode 100644 index 0000000..3d4b316 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/account/command/rank/arguments/ListArgument.java @@ -0,0 +1,28 @@ +package zone.themcgamer.core.account.command.rank.arguments; + +import org.bukkit.command.CommandSender; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.data.Rank; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author Braydon + */ +public class ListArgument { + @Command(name = "rank.list", description = "View a list of ranks", ranks = { Rank.MODERATOR }) + public void onCommand(CommandProvider command) { + CommandSender sender = command.getSender(); + List ranks = Arrays.asList(Rank.values()); + if (ranks.isEmpty()) { + sender.sendMessage(Style.error("Rank", "There are no ranks to list.")); + return; + } + sender.sendMessage(Style.main("Rank", "Showing &f" + ranks.size() + " &7ranks: &f" + + ranks.stream().map(Rank::getDisplayName).collect(Collectors.joining("&7, &f")))); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/account/command/rank/arguments/SetArgument.java b/core/src/main/java/zone/themcgamer/core/account/command/rank/arguments/SetArgument.java new file mode 100644 index 0000000..8a39536 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/account/command/rank/arguments/SetArgument.java @@ -0,0 +1,62 @@ +package zone.themcgamer.core.account.command.rank.arguments; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import zone.themcgamer.core.account.Account; +import zone.themcgamer.core.account.AccountManager; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.data.Rank; + +import java.util.Optional; + +/** + * @author Braydon + */ +@AllArgsConstructor @Getter +public class SetArgument { + private final AccountManager accountManager; + + @Command(name = "rank.set", usage = " ", description = "Set the rank of a player", ranks = { Rank.ADMIN }) + public void onCommand(CommandProvider command) { + CommandSender sender = command.getSender(); + String[] args = command.getArgs(); + if (args.length < 2) { + sender.sendMessage(Style.main("Rank", "Usage: /rank set ")); + return; + } + Optional optionalRank = Rank.lookup(args[1]); + if (!optionalRank.isPresent()) { + sender.sendMessage(Style.error("Rank", "§cThat rank does not exist.")); + return; + } + Rank rank = optionalRank.get(); + if (rank.getCategory() == Rank.RankCategory.SUB) { + sender.sendMessage(Style.error("Rank", "§cA player cannot have their primary rank set to a sub rank.")); + return; + } + if (command.isPlayer()) { + Optional optionalAccount = AccountManager.fromCache(((Player) sender).getUniqueId()); + if (!optionalAccount.isPresent()) { + sender.sendMessage(Style.error("Rank", "§cError whilst fetching account, please try again later...")); + return; + } + Account account = optionalAccount.get(); + if (account.getPrimaryRank() == rank || !account.hasRank(rank)) { + sender.sendMessage(Style.error("Rank", "§cYou cannot set a player's rank to a rank higher or equal to than your own.")); + return; + } + } + accountManager.lookup(args[0], account -> { + if (account == null) { + sender.sendMessage(Style.invalidAccount("Rank", args[0])); + return; + } + sender.sendMessage(Style.main("Rank", "Updated " + account.getDisplayName() + "'s §7rank to §f" + rank.getColor() + rank.getDisplayName())); + accountManager.setRank(account, rank); + }); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/account/event/AccountLoadEvent.java b/core/src/main/java/zone/themcgamer/core/account/event/AccountLoadEvent.java new file mode 100644 index 0000000..9bf7e69 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/account/event/AccountLoadEvent.java @@ -0,0 +1,16 @@ +package zone.themcgamer.core.account.event; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import zone.themcgamer.core.account.Account; +import zone.themcgamer.core.common.WrappedBukkitEvent; + +/** + * @author Braydon + * @implNote This event is called when an {@link Account} is finished loading + */ +@AllArgsConstructor @Setter @Getter +public class AccountLoadEvent extends WrappedBukkitEvent { + private final Account account; +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/account/event/AccountPreLoadEvent.java b/core/src/main/java/zone/themcgamer/core/account/event/AccountPreLoadEvent.java new file mode 100644 index 0000000..8386613 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/account/event/AccountPreLoadEvent.java @@ -0,0 +1,19 @@ +package zone.themcgamer.core.account.event; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import zone.themcgamer.core.account.Account; +import zone.themcgamer.core.common.WrappedBukkitEvent; + +import java.util.UUID; + +/** + * @author Braydon + * @implNote This event is called when an {@link Account} is finished loading + */ +@AllArgsConstructor @Setter @Getter +public class AccountPreLoadEvent extends WrappedBukkitEvent { + private final UUID uuid; + private final String name, ipAddress; +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/account/event/AccountUnloadEvent.java b/core/src/main/java/zone/themcgamer/core/account/event/AccountUnloadEvent.java new file mode 100644 index 0000000..a99d835 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/account/event/AccountUnloadEvent.java @@ -0,0 +1,16 @@ +package zone.themcgamer.core.account.event; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import zone.themcgamer.core.account.Account; +import zone.themcgamer.core.common.WrappedBukkitEvent; + +/** + * @author Braydon + * @implNote This event is called when an {@link Account} is unloaded + */ +@AllArgsConstructor @Setter @Getter +public class AccountUnloadEvent extends WrappedBukkitEvent { + private final Account account; +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/account/menu/ProfileMenu.java b/core/src/main/java/zone/themcgamer/core/account/menu/ProfileMenu.java new file mode 100644 index 0000000..e48a0d7 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/account/menu/ProfileMenu.java @@ -0,0 +1,77 @@ +package zone.themcgamer.core.account.menu; + +import com.cryptomorin.xseries.XMaterial; +import org.bukkit.entity.Player; +import zone.themcgamer.common.DoubleUtils; +import zone.themcgamer.core.account.Account; +import zone.themcgamer.core.account.AccountManager; +import zone.themcgamer.core.common.ItemBuilder; +import zone.themcgamer.core.common.menu.Button; +import zone.themcgamer.core.common.menu.Menu; +import zone.themcgamer.core.common.menu.MenuType; + +import java.util.Optional; + +public class ProfileMenu extends Menu { + public ProfileMenu(Player player) { + super(player, "Profile » " + player.getName(), 3, MenuType.CHEST); + } + + @Override + protected void onOpen() { + Optional optionalAccount = AccountManager.fromCache(player.getUniqueId()); + if (!optionalAccount.isPresent()) + return; + Account account = optionalAccount.get(); + set(1, 1, new Button(new ItemBuilder(XMaterial.PLAYER_HEAD) + .setSkullOwner(player.getName()) + .setName("§f" + account.getDisplayName() + "'s §a§lProfile") + .setLore( + "", + "&fRank &7» &f" + account.getPrimaryRank().getColor() + account.getPrimaryRank().getDisplayName(), + "&fGold &7» &6" + DoubleUtils.format(account.getGold(), true) + " ⛃", + "&fGems &7» &2" + DoubleUtils.format(account.getGems(), true) + " ✦", + "", + "&fCoin Multiplier &7» &bx1.0", + "", + "&aClick to view your stats" + ).toItemStack())); + + set(1, 3, new Button(new ItemBuilder(XMaterial.EXPERIENCE_BOTTLE) + .setName("&b&lNetwork Level") + .setLore( + "&f&lYou are now level &60", + "&fProgress &a||||&7|||||||||||", + "", + "&7By playing games you will", + "&7receive &dexperience &7points.", + "&7By leveling up you will receive", + "&7various features unlocked!", + "", + "&aClick to view rewards" + ).toItemStack())); + + set(1, 4, new Button(new ItemBuilder(XMaterial.COMPARATOR) + .setName("&b&lSettings") + .setLore( + "", + "&7Here you can modify", + "&7account settings for features", + "&7across the network.", + "", + "&aClick to modify your settings" + ).toItemStack())); + + set(1, 5, new Button(new ItemBuilder(XMaterial.EMERALD) + .setName("&2&lGem Boxes") + .setLore( + "&fYou currently have &c0 &fgem boxes!", + "", + "&7Collect &2gem&7 boxes", + "&7by &aplaying&7 different games", + "&7and completing &9missions &7& &6achievements&7.", + "", + "&aClick to view your boxes" + ).toItemStack())); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/animation/TextAnimation.java b/core/src/main/java/zone/themcgamer/core/animation/TextAnimation.java new file mode 100644 index 0000000..77c3abe --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/animation/TextAnimation.java @@ -0,0 +1,19 @@ +package zone.themcgamer.core.animation; + +import lombok.Getter; + +/** + * @author Braydon + * @implNote Simple text animation base + */ +@Getter +public abstract class TextAnimation { + private final String input; + protected int index; + + public TextAnimation(String input) { + this.input = input; + } + + public abstract String next(); +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/animation/impl/WaveAnimation.java b/core/src/main/java/zone/themcgamer/core/animation/impl/WaveAnimation.java new file mode 100644 index 0000000..8f7614d --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/animation/impl/WaveAnimation.java @@ -0,0 +1,97 @@ +package zone.themcgamer.core.animation.impl; + +import org.bukkit.ChatColor; +import zone.themcgamer.core.animation.TextAnimation; + +/** + * @author Braydon + * @implNote Simple to use wave animation. + */ +public class WaveAnimation extends TextAnimation { + private String primary, secondary, tertiary; + private boolean bold; + + public WaveAnimation(String input) { + super(input); + primary = secondary = tertiary = ""; + } + + /** + * Add a primary color to the animation + * @param color the color to add + */ + public WaveAnimation withPrimary(String color) { + primary = color; + return this; + } + + /** + * Add a secondary color to the animation + * @param color the color to add + */ + public WaveAnimation withSecondary(String color) { + secondary = color; + return this; + } + + /** + * Add a third (highlight) color to the animation + * @param color the color to add + */ + public WaveAnimation withTertiary(String color) { + tertiary = color; + return this; + } + + /** + * Make the animation text bold + */ + public WaveAnimation withBold() { + bold = true; + return this; + } + + /** + * Animate the animation and return the new text + * @return the text + */ + @Override + public String next() { + String[] chars = new String[getInput().length() * 2]; + String[] primaryRun = getFrames(primary, secondary); + String[] secondaryRun = getFrames(secondary, primary); + + System.arraycopy(primaryRun, 0, chars, 0, getInput().length()); + System.arraycopy(secondaryRun, 0, chars, getInput().length(), getInput().length()); + + String primary = chars[index]; + if (++index >= chars.length) + index = 0; + return primary; + } + + /** + * Get the frames for the text with the given primary and secondary colors + * @param primary the primary color + * @param secondary the secondary color + * @return the frames + */ + private String[] getFrames(String primary, String secondary) { + String[] output = new String[getInput().length()]; + for (int i = 0; i < getInput().length(); i++) { + StringBuilder builder = new StringBuilder(getInput().length() * 3) + .append(primary).append(bold ? ChatColor.BOLD.toString() : ""); + for (int j = 0; j < getInput().length(); j++) { + char c = getInput().charAt(j); + if (j == i) { + builder.append(tertiary).append(bold ? ChatColor.BOLD.toString() : ""); + } else if (j == i + 1) { + builder.append(secondary).append(bold ? ChatColor.BOLD.toString() : ""); + } + builder.append(c); + } + output[i] = builder.toString(); + } + return output; + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/badSportSystem/BadSportClient.java b/core/src/main/java/zone/themcgamer/core/badSportSystem/BadSportClient.java new file mode 100644 index 0000000..d0ebda4 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/badSportSystem/BadSportClient.java @@ -0,0 +1,60 @@ +package zone.themcgamer.core.badSportSystem; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; + +import java.util.*; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * @author Braydon + */ +@RequiredArgsConstructor @Setter @Getter +public class BadSportClient { + private final String ip; + private Set punishments = new HashSet<>(); + + public Collection getPastOffenses(PunishmentOffense offense) { + return filter(punishment -> punishment.getOffense() == offense && !punishment.wasRemoved()); + } + + /** + * Get the currently active ban + * @return the optional ban + */ + public Optional getBan() { + return filterOne(punishment -> punishment.isActive() + && (punishment.getCategory() == PunishmentCategory.BAN || punishment.getCategory() == PunishmentCategory.BLACKLIST)); + } + + /** + * Get the currently active mute + * @return the optional mute + */ + public Optional getMute() { + return filterOne(punishment -> punishment.isActive() && (punishment.getCategory() == PunishmentCategory.MUTE)); + } + + /** + * Get an optional {@link Punishment} that matches against the {@link Predicate} + * @param predicate the predicate to test against + * @return the optional punishment + */ + public Optional filterOne(Predicate predicate) { + List punishments = new ArrayList<>(filter(predicate)); + if (punishments.isEmpty()) + return Optional.empty(); + return Optional.of(punishments.get(0)); + } + + /** + * Get a {@link Collection} of punishments that matches against the {@link Predicate} + * @param predicate the predicate to test against + * @return the collection of punishments + */ + public Collection filter(Predicate predicate) { + return punishments.stream().filter(predicate).collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/badSportSystem/BadSportRepository.java b/core/src/main/java/zone/themcgamer/core/badSportSystem/BadSportRepository.java new file mode 100644 index 0000000..13564ba --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/badSportSystem/BadSportRepository.java @@ -0,0 +1,97 @@ +package zone.themcgamer.core.badSportSystem; + +import com.zaxxer.hikari.HikariDataSource; +import zone.themcgamer.data.mysql.data.column.Column; +import zone.themcgamer.data.mysql.data.column.impl.IntegerColumn; +import zone.themcgamer.data.mysql.data.column.impl.LongColumn; +import zone.themcgamer.data.mysql.data.column.impl.StringColumn; +import zone.themcgamer.data.mysql.repository.MySQLRepository; + +import java.sql.SQLException; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; + +/** + * @author Braydon + */ +public class BadSportRepository extends MySQLRepository { + private static final String INSERT_PUNISHMENT = "INSERT INTO `punishments` " + + "(`id`, `targetIp`, `targetUuid`, `category`, `offense`, `severity`, `staffUuid`, `staffName`, `timeIssued`, `duration`, `reason`, `removeStaffUuid`, `removeStaffName`, `removeReason`, `timeRemoved`) VALUES " + + "(NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, NULL, '-1');"; + private static final String REMOVE_PUNISHMENT = "UPDATE `punishments` SET `removeStaffUuid` = ?, `removeStaffName` = ?, `removeReason` = ?, `timeRemoved` = ? WHERE `id` = ?;"; + + public BadSportRepository(HikariDataSource dataSource) { + super(dataSource); + } + + /** + * Post a {@link Punishment} to MySQL + * @param punishment the punishment + */ + public void punish(Punishment punishment, Consumer idConsumer) { + punish( + punishment.getTargetIp(), + punishment.getTargetUuid(), + punishment.getCategory(), + punishment.getOffense(), + punishment.getSeverity(), + punishment.getStaffUuid(), + punishment.getStaffName(), + punishment.getTimeIssued(), + punishment.getDuration(), + punishment.getReason(), + idConsumer + ); + } + + /** + * Post a {@link Punishment} to MySQL + * @param encryptedIpAddress the target encrypted ip of the punishment + * @param uuid the target uuid of the punishment + * @param category the category of the punishment + * @param offense the offense of the punishment + * @param staffUuid the staff uuid of the punishment + * @param staffName the staff name of the punishment + * @param timeIssued the time the punishment was issued + * @param duration the duration of the punishment + * @param reason the reason of the punishment + */ + public void punish(String encryptedIpAddress, UUID uuid, PunishmentCategory category, PunishmentOffense offense, int severity, + UUID staffUuid, String staffName, long timeIssued, long duration, String reason, Consumer idConsumer) { + CompletableFuture.runAsync(() -> { + executeInsert(INSERT_PUNISHMENT, new Column[] { + new StringColumn("targetIp", encryptedIpAddress), + new StringColumn("targetUuid", uuid.toString()), + new StringColumn("category", category.name()), + new StringColumn("offense", offense.name()), + new IntegerColumn("severity", severity), + new StringColumn("staffUuid", staffUuid.toString()), + new StringColumn("staffName", staffName), + new LongColumn("timeIssued", timeIssued), + new LongColumn("duration", duration), + new StringColumn("reason", reason) + }, resultSet -> { + try { + while (resultSet.next()) { + idConsumer.accept(resultSet.getInt(1)); + } + } catch (SQLException ex) { + ex.printStackTrace(); + } + }); + }); + } + + public void remove(Punishment punishment) { + CompletableFuture.runAsync(() -> { + executeInsert(REMOVE_PUNISHMENT, new Column[] { + new StringColumn("removeStaffUuid", punishment.getRemoveStaffUuid() == null ? null : punishment.getRemoveStaffUuid().toString()), + new StringColumn("removeStaffName", punishment.getRemoveStaffName() == null ? null : punishment.getRemoveStaffName()), + new StringColumn("removeReason", punishment.getRemoveReason() == null ? null : punishment.getRemoveReason()), + new LongColumn("timeRemoved", punishment.getTimeRemoved()), + new IntegerColumn("id", punishment.getId()) + }); + }); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/badSportSystem/BadSportSystem.java b/core/src/main/java/zone/themcgamer/core/badSportSystem/BadSportSystem.java new file mode 100644 index 0000000..fe3b167 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/badSportSystem/BadSportSystem.java @@ -0,0 +1,247 @@ +package zone.themcgamer.core.badSportSystem; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.player.PlayerLoginEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.Nullable; +import zone.themcgamer.common.EnumUtils; +import zone.themcgamer.common.MiscUtils; +import zone.themcgamer.common.TimeUtils; +import zone.themcgamer.core.account.Account; +import zone.themcgamer.core.account.AccountManager; +import zone.themcgamer.core.account.MiniAccount; +import zone.themcgamer.core.account.event.AccountPreLoadEvent; +import zone.themcgamer.core.badSportSystem.command.BadSportCommand; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.core.module.Module; +import zone.themcgamer.core.module.ModuleInfo; +import zone.themcgamer.data.Rank; +import zone.themcgamer.data.jedis.command.JedisCommandHandler; +import zone.themcgamer.data.jedis.command.impl.PlayerKickCommand; +import zone.themcgamer.data.jedis.command.impl.PlayerMessageCommand; +import zone.themcgamer.data.jedis.command.impl.PunishmentsUpdateCommand; +import zone.themcgamer.data.jedis.command.impl.RankMessageCommand; +import zone.themcgamer.data.mysql.MySQLController; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.*; +import java.util.stream.Collectors; + +/** + * @author Braydon + */ +@ModuleInfo(name = "Bad Sport System") +public class BadSportSystem extends Module { + private static final Gson GSON = new GsonBuilder() + .registerTypeAdapter(Punishment.class, new PunishmentSerializer()) + .excludeFieldsWithoutExposeAnnotation().create(); + + private final Map accounts = new HashMap<>(); + private final BadSportRepository repository; + + public BadSportSystem(JavaPlugin plugin, MySQLController mySQLController, AccountManager accountManager) { + super(plugin); + AccountManager.addMiniAccount(new IPPunishmentLoader(plugin)); + AccountManager.addMiniAccount(new AccountsPunishmentLoader(plugin)); + repository = new BadSportRepository(mySQLController.getDataSource()); + JedisCommandHandler.getInstance().addListener(jedisCommand -> { + if (jedisCommand instanceof PunishmentsUpdateCommand) { + PunishmentsUpdateCommand punishmentsUpdateCommand = (PunishmentsUpdateCommand) jedisCommand; + Optional optionalBadSportClient = lookup(punishmentsUpdateCommand.getUuid()); + if (!optionalBadSportClient.isPresent()) + return; + try { + Set punishments = GSON.fromJson(punishmentsUpdateCommand.getJson(), new TypeToken>() {}.getType()); + optionalBadSportClient.get().setPunishments(punishments); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + }); + registerCommand(new BadSportCommand(accountManager)); + } + + @EventHandler + private void onAccountPreLoad(AccountPreLoadEvent event) { + accounts.put(event.getUuid(), new BadSportClient(event.getIpAddress())); + } + + @EventHandler(priority = EventPriority.HIGHEST) + private void onLogin(PlayerLoginEvent event) { + Optional optionalClient = lookup(event.getPlayer().getUniqueId()); + if (!optionalClient.isPresent()) + return; + Optional optionalPunishment = optionalClient.get().getBan(); + if (!optionalPunishment.isPresent()) + return; + Punishment punishment = optionalPunishment.get(); + event.disallow(PlayerLoginEvent.Result.KICK_BANNED, PunishmentCategory.format(punishment)); + } + + @EventHandler + private void onQuit(PlayerQuitEvent event) { + accounts.remove(event.getPlayer().getUniqueId()); + } + + public void punish(String encryptedIpAddress, UUID uuid, String name, PunishmentCategory category, PunishmentOffense offense, + int severity, @Nullable UUID staffUuid, String staffName, long duration, String reason, boolean silent) { + Punishment punishment = new Punishment(encryptedIpAddress, uuid, category, offense, severity, staffUuid, staffName, + System.currentTimeMillis(), duration, reason); + BadSportClient client = accounts.get(uuid); + if (client != null) { + for (Punishment previousPunishment : client.getPunishments().stream() + .filter(p -> p.getCategory() == category && p.isActive()) + .collect(Collectors.toList())) { + remove(previousPunishment, name, null, null, null, true, true); + } + client.getPunishments().add(punishment); + } + repository.punish(punishment, id -> { + punishment.setId(id); + if (client != null) + JedisCommandHandler.getInstance().send(new PunishmentsUpdateCommand(uuid, GSON.toJson(client.getPunishments()))); + }); + + if (category.isKick()) + JedisCommandHandler.getInstance().send(new PlayerKickCommand(uuid, PunishmentCategory.format(punishment))); + + if (staffUuid != null) { + Optional optionalAccount = AccountManager.fromCache(staffUuid); + if (optionalAccount.isPresent()) + staffName = optionalAccount.get().getDisplayName(); + } + JedisCommandHandler.getInstance().send(new RankMessageCommand(silent ? Rank.HELPER : Rank.DEFAULT, Style.main( + "Bad Sport" + (silent ? " §7(Silent)" : ""), + "§f" + name + " §7was " + category.getIssuedMessage() + " by §f" + staffName + + (category.isHasDuration() ? " §7for §f" + TimeUtils.convertString(duration) : "") + ))); + if (category == PunishmentCategory.WARN || category == PunishmentCategory.MUTE) { + JedisCommandHandler.getInstance().send(new PlayerMessageCommand(uuid, Style.main("Bad Sport", + "You were " + category.getIssuedMessage() + + (category.isHasDuration() ? " §7for §f" + TimeUtils.convertString(duration) : "") + " §7because of §f" + reason + ))); + } + } + + public void remove(Punishment punishment, String name, UUID staffUuid, String staffName, String reason, boolean silent, boolean removingPrevious) { + punishment.remove(staffUuid, staffName, reason); + repository.remove(punishment); + BadSportClient client = accounts.get(punishment.getTargetUuid()); + if (client != null && !removingPrevious) + JedisCommandHandler.getInstance().send(new PunishmentsUpdateCommand(punishment.getTargetUuid(), GSON.toJson(client.getPunishments()))); + if (removingPrevious) + return; + PunishmentCategory category = punishment.getCategory(); + if (category.getRemovedMessage() == null) + return; + if (staffUuid != null) { + Optional optionalAccount = AccountManager.fromCache(staffUuid); + if (optionalAccount.isPresent()) + staffName = optionalAccount.get().getDisplayName(); + } + JedisCommandHandler.getInstance().send(new RankMessageCommand(silent ? Rank.HELPER : Rank.DEFAULT, Style.main( + "Bad Sport" + (silent ? " §7(Silent)" : ""), + "§f" + name + " §7was " + category.getRemovedMessage() + " by §f" + staffName) + )); + } + + /** + * Get the {@link BadSportClient} account for the given {@link UUID} + * @param uuid the uuid of the account + * @return the optional account + */ + public Optional lookup(UUID uuid) { + return Optional.ofNullable(accounts.get(uuid)); + } + + private List getPunishments(String encryptedIp, ResultSet resultSet) { + List punishments = new ArrayList<>(); + try { + while (resultSet.next()) { + PunishmentCategory category = EnumUtils.fromString(PunishmentCategory.class, resultSet.getString("category")); + if (category == null) + continue; + PunishmentOffense offense = EnumUtils.fromString(PunishmentOffense.class, resultSet.getString("offense")); + if (offense == null) + continue; + punishments.add(new Punishment( + resultSet.getInt("id"), + encryptedIp, + UUID.fromString(resultSet.getString("targetUuid")), + category, + offense, + resultSet.getInt("severity"), + MiscUtils.getUuid(resultSet.getString("staffUuid")), + resultSet.getString("staffName"), + resultSet.getLong("timeIssued"), + resultSet.getLong("duration"), + resultSet.getString("reason"), + MiscUtils.getUuid(resultSet.getString("removeStaffUuid")), + resultSet.getString("removeStaffName"), + resultSet.getString("removeReason"), + resultSet.getLong("timeRemoved") + )); + } + } catch (SQLException ex) { + ex.printStackTrace(); + } + return punishments; + } + + @ModuleInfo(name = "BSS IP Punishments Loader") + private class IPPunishmentLoader extends MiniAccount { + public IPPunishmentLoader(JavaPlugin plugin) { + super(plugin); + } + + @Override + public BadSportClient getAccount(int accountId, UUID uuid, String name, String ip, String encryptedIp) { + return null; + } + + @Override + public String getQuery(int accountId, UUID uuid, String name, String ip, String encryptedIp) { + return "SELECT * FROM `punishments` WHERE `targetIp` = '" + encryptedIp + "';"; + } + + @Override + public void loadAccount(int accountId, UUID uuid, String name, String ip, String encryptedIp, ResultSet resultSet) { + if (ip.trim().isEmpty()) + return; + System.out.println("Fetching punishments for ip \"" + encryptedIp + "\" (\"" + ip + "\")"); + accounts.values().stream().filter(badSportClient -> badSportClient.getIp().equals(ip)).findFirst() + .ifPresent(client -> client.getPunishments().addAll(getPunishments(encryptedIp, resultSet))); + } + } + + @ModuleInfo(name = "BSS Account Punishments Loader") + private class AccountsPunishmentLoader extends MiniAccount { + public AccountsPunishmentLoader(JavaPlugin plugin) { + super(plugin); + } + + @Override + public BadSportClient getAccount(int accountId, UUID uuid, String name, String ip, String encryptedIp) { + return null; + } + + @Override + public String getQuery(int accountId, UUID uuid, String name, String ip, String encryptedIp) { + return "SELECT * FROM `punishments` WHERE `targetUuid` = '" + uuid.toString() + "';"; + } + + @Override + public void loadAccount(int accountId, UUID uuid, String name, String ip, String encryptedIp, ResultSet resultSet) { + System.out.println("Fetching punishments for account \"" + uuid.toString() + "\" (\"" + name + "\")"); + BadSportClient client = accounts.get(uuid); + if (client != null) + client.getPunishments().addAll(getPunishments(encryptedIp, resultSet)); + } + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/badSportSystem/Punishment.java b/core/src/main/java/zone/themcgamer/core/badSportSystem/Punishment.java new file mode 100644 index 0000000..44333d1 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/badSportSystem/Punishment.java @@ -0,0 +1,90 @@ +package zone.themcgamer.core.badSportSystem; + +import lombok.*; + +import java.util.Objects; +import java.util.UUID; + +/** + * @author Braydon + */ +@AllArgsConstructor @RequiredArgsConstructor @Getter @ToString +public class Punishment { + @Setter private int id; + private final String targetIp; + private final UUID targetUuid; + private final PunishmentCategory category; + private final PunishmentOffense offense; + private final int severity; + private final UUID staffUuid; + private final String staffName; + private final long timeIssued, duration; + private final String reason; + private UUID removeStaffUuid; + private String removeStaffName, removeReason; + private long timeRemoved = -1L; + + public void remove(UUID staffUuid, String staffName, String reason) { + if (!isActive()) + throw new IllegalStateException("Cannot remove punishment, it's not active"); + removeStaffUuid = staffUuid; + removeStaffName = staffName; + removeReason = reason; + timeRemoved = System.currentTimeMillis(); + } + + public boolean isIP() { + return category.isIp(); + } + + public boolean issuedByConsole() { + return staffUuid == null; + } + + public boolean isActive() { + if (category == PunishmentCategory.KICK || category == PunishmentCategory.WARN) + return false; + if (wasOverriden()) + return false; + if (wasRemoved()) + return false; + if (isPermanent()) + return true; + return !hasExpired(); + } + + public boolean hasExpired() { + return getRemaining() <= 0L; + } + + public long getRemaining() { + return isPermanent() ? -1L : (timeIssued + duration) - System.currentTimeMillis(); + } + + public boolean isPermanent() { + return duration == -1L; + } + + public boolean wasOverriden() { + return removeStaffUuid == null && removeStaffName == null && timeRemoved != -1L; + } + + public boolean wasRemoved() { + return removeStaffName != null && timeRemoved != -1L; + } + + @Override + public boolean equals(Object other) { + if (this == other) + return true; + if (other == null || getClass() != other.getClass()) + return false; + Punishment that = (Punishment) other; + return id == that.id; + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/badSportSystem/PunishmentCategory.java b/core/src/main/java/zone/themcgamer/core/badSportSystem/PunishmentCategory.java new file mode 100644 index 0000000..77273f0 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/badSportSystem/PunishmentCategory.java @@ -0,0 +1,98 @@ +package zone.themcgamer.core.badSportSystem; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import zone.themcgamer.common.MiscUtils; +import zone.themcgamer.common.TimeUtils; + +/** + * @author Braydon + */ +@AllArgsConstructor @RequiredArgsConstructor @Getter +public enum PunishmentCategory { + KICK("Kick", "kicked", null, false, true, false, MiscUtils.arrayToString( + "§2§lMc§6§lGamer§c§lZone §8» §a§lPunishment", + "", + "§7You have been §c§lKicked§7!", + "§7Reason: §a$reason", + "", + "§7Kicked by:", + "§c$staff" + )), + WARN("Warn", "warned", null, false, false, false, "You were warned by §6$staff §7for §f$reason"), + MUTE("Mute", "muted", "unmuted", false, false, true, + "You were muted by §6$staff §7for §b$duration §7because of §f$reason", + "You were permanently muted by §6$staff §7because of §f$reason" + ), + BAN("Ban", "banned", "unbanned", false, true, true, MiscUtils.arrayToString( + "§2§lMc§6§lGamer§c§lZone §8» §a§lPunishment", + "", + "§7You have been §c§lBanned§7!", + "§7Reason: §a$reason", + "", + "§7Release date:", + "§6$time", + "", + "§7Banned by:", + "§c$staff", + "", + "§7Expires in:", + "§6$duration", + "", + "§7If you believe this is an §6error§7, please", + "§6create §7a support ticket on our §6website", + "§bwww.mcgamerzone.net" + ), MiscUtils.arrayToString( + "§2§lMc§6§lGamer§c§lZone §8» §a§lPunishment", + "", + "§7You have been §c§lBanned§7!", + "§7Reason: §a$reason", + "", + "§7Release date:", + "§6$time", + "", + "§7Banned by:", + "§c$staff", + "", + "§c§lThis punishment is permanent!", + "§7If you believe this is an §6error§7, please", + "§6create §7a support ticket on our §6website", + "§bwww.mcgamerzone.net" + )), + BLACKLIST("Blacklist", "blacklisted", "unblacklisted", true, true, true, null, MiscUtils.arrayToString( + "§2§lMc§6§lGamer§c§lZone §8» §a§lPunishment", + "", + "§7You have been §c§lBlacklisted§7!", + "§7Reason: §a$reason", + "", + "§7Release date:", + "§6$time", + "", + "§7Banned by:", + "§c$staff", + "", + "§c§lThis punishment is permanent!", + "§7If you believe this is an §6error§7, please", + "§6create §7a support ticket on our §6website", + "§bwww.mcgamerzone.net" + )); + + private final String name, issuedMessage, removedMessage; + private final boolean ip, kick, hasDuration; + private String temporaryMessage; + private final String permanentMessage; + + public static String format(Punishment punishment) { + PunishmentCategory category = punishment.getCategory(); + String message; + if (category.getTemporaryMessage() == null) + message = category.getPermanentMessage(); + else message = punishment.isPermanent() ? category.getPermanentMessage() : category.getTemporaryMessage(); + message = message.replace("$staff", punishment.getStaffName()); + message = message.replace("$time", TimeUtils.when(punishment.getTimeIssued())); + message = message.replace("$duration", TimeUtils.formatIntoDetailedString(punishment.getRemaining(), false)); + message = message.replace("$reason", punishment.getReason()); + return message; + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/badSportSystem/PunishmentOffense.java b/core/src/main/java/zone/themcgamer/core/badSportSystem/PunishmentOffense.java new file mode 100644 index 0000000..eff489b --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/badSportSystem/PunishmentOffense.java @@ -0,0 +1,61 @@ +package zone.themcgamer.core.badSportSystem; + +import com.cryptomorin.xseries.XMaterial; +import lombok.AllArgsConstructor; +import lombok.Getter; +import zone.themcgamer.common.Tuple; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * @author Braydon + */ +@AllArgsConstructor @Getter +public enum PunishmentOffense { + KICK("Kick", XMaterial.LEATHER_BOOTS, (byte) 0, 0, new Tuple[] {}), + WARNING("Warning", XMaterial.PAPER, (byte) 0, 0, new Tuple[] {}), + PERMANENT_MUTE("Permanent Mute", XMaterial.WRITABLE_BOOK, (byte) 0, 0, new Tuple[] {}), + PERMANENT_BAN("Permanent Ban", XMaterial.REDSTONE_BLOCK, (byte) 0, 0, new Tuple[] {}), + CHAT("Chat", XMaterial.WRITABLE_BOOK, (byte) 0, 3, new Tuple[] { + new Tuple<>(PunishmentCategory.MUTE, TimeUnit.SECONDS.toMillis(20L)) + }), + GAMEPLAY("Gameplay", XMaterial.HOPPER, (byte) 0, 1, new Tuple[] { + new Tuple<>(PunishmentCategory.MUTE, TimeUnit.SECONDS.toMillis(20L)) + }), + HACKING("Hacking", XMaterial.IRON_SWORD, (byte) 0, 3, new Tuple[] { + new Tuple<>(PunishmentCategory.BAN, TimeUnit.DAYS.toMillis(7L)), + new Tuple<>(PunishmentCategory.BAN, TimeUnit.DAYS.toMillis(30L)), + new Tuple<>(PunishmentCategory.BAN, -1L), + }); + + private final String name; + private final XMaterial icon; + private final byte data; + private final int severities; + private final Tuple[] durations; + + public Tuple calculatePunishmentDuration(BadSportClient client, int severiy) { + List pastPunishments = new ArrayList<>(client.getPastOffenses(this)); + long lastDuration = durations[0].getRight(); + if (!pastPunishments.isEmpty()) { // If the last duration is -1, loop through the punishments and try and find the last duration for this offense + pastPunishments.sort((a, b) -> Long.compare(b.getTimeIssued(), a.getTimeIssued())); + lastDuration = pastPunishments.get(0).getDuration(); + } + Tuple durationTuple; + if (pastPunishments.size() < durations.length) { + durationTuple = durations[pastPunishments.size()]; + if (severiy > 1 && (severiy <= durations.length)) + durationTuple = durations[severiy - 1]; + else if (severiy > 1) durationTuple.setRight(durationTuple.getRight() + calculatePunishmentDuration(client, severiy - 1).getRight()); + } else { + durationTuple = durations[durations.length - 1].clone(); + long duration = lastDuration; + if (lastDuration != -1L) + duration = lastDuration * 2L; + durationTuple.setRight(duration * severiy); + } + return durationTuple; + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/badSportSystem/PunishmentSerializer.java b/core/src/main/java/zone/themcgamer/core/badSportSystem/PunishmentSerializer.java new file mode 100644 index 0000000..f67ae3c --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/badSportSystem/PunishmentSerializer.java @@ -0,0 +1,59 @@ +package zone.themcgamer.core.badSportSystem; + +import com.google.gson.*; +import zone.themcgamer.common.MiscUtils; + +import java.lang.reflect.Type; +import java.util.UUID; + +/** + * @author Braydon + */ +public class PunishmentSerializer implements JsonSerializer, JsonDeserializer { + @Override + public Punishment deserialize(JsonElement element, Type type, JsonDeserializationContext context) throws JsonParseException { + if (!element.isJsonObject()) + return null; + JsonObject object = (JsonObject) element; + String removeStaffName = object.get("removeStaffName").getAsString(); + String removeReason = object.get("removeReason").getAsString(); + return new Punishment( + object.get("id").getAsInt(), + object.get("targetIp").getAsString(), + UUID.fromString(object.get("targetUuid").getAsString()), + PunishmentCategory.valueOf(object.get("category").getAsString()), + PunishmentOffense.valueOf(object.get("offense").getAsString()), + object.get("severity").getAsInt(), + MiscUtils.getUuid(object.get("staffUuid").getAsString()), + object.get("staffName").getAsString(), + object.get("timeIssued").getAsLong(), + object.get("duration").getAsLong(), + object.get("reason").getAsString(), + MiscUtils.getUuid(object.get("removeStaffUuid").getAsString()), + removeStaffName.trim().isEmpty() ? null : removeStaffName, + removeReason.trim().isEmpty() ? null : removeReason, + object.get("timeRemoved").getAsLong() + ); + } + + @Override + public JsonElement serialize(Punishment punishment, Type type, JsonSerializationContext context) { + JsonObject object = new JsonObject(); + object.addProperty("id", punishment.getId()); + object.addProperty("targetIp", punishment.getTargetIp()); + object.addProperty("targetUuid", punishment.getTargetUuid().toString()); + object.addProperty("category", punishment.getCategory().name()); + object.addProperty("offense", punishment.getOffense().name()); + object.addProperty("severity", punishment.getSeverity()); + object.addProperty("staffUuid", punishment.getStaffUuid() == null ? "" : punishment.getStaffUuid().toString()); + object.addProperty("staffName", punishment.getStaffName()); + object.addProperty("timeIssued", punishment.getTimeIssued()); + object.addProperty("duration", punishment.getDuration()); + object.addProperty("reason", punishment.getReason()); + object.addProperty("removeStaffUuid", punishment.getRemoveStaffUuid() == null ? "" : punishment.getRemoveStaffUuid().toString()); + object.addProperty("removeStaffName", punishment.getRemoveStaffName() == null ? "" : punishment.getRemoveStaffName()); + object.addProperty("removeReason", punishment.getRemoveReason() == null ? "" : punishment.getRemoveReason()); + object.addProperty("timeRemoved", punishment.getTimeRemoved()); + return object; + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/badSportSystem/command/BadSportCommand.java b/core/src/main/java/zone/themcgamer/core/badSportSystem/command/BadSportCommand.java new file mode 100644 index 0000000..9f8b6aa --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/badSportSystem/command/BadSportCommand.java @@ -0,0 +1,51 @@ +package zone.themcgamer.core.badSportSystem.command; + +import lombok.AllArgsConstructor; +import org.bukkit.entity.Player; +import zone.themcgamer.core.account.Account; +import zone.themcgamer.core.account.AccountManager; +import zone.themcgamer.core.badSportSystem.menu.BadSportMenu; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.data.Rank; + +import java.util.Arrays; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * @author Braydon + */ +@AllArgsConstructor +public class BadSportCommand { + private final AccountManager accountManager; + + @Command(name = "badsport", aliases = { "bss" }, description = "Punish a player", ranks = { Rank.HELPER }, playersOnly = true) + public void onCommand(CommandProvider command) { + Player player = command.getPlayer(); + String[] args = command.getArgs(); + if (args.length < 1) { + player.sendMessage(Style.main("Bad Sport", "Usage: /" + command.getLabel() + " [reason] [-s]")); + return; + } + Optional optionalExecutorAccount = AccountManager.fromCache(player.getUniqueId()); + if (!optionalExecutorAccount.isPresent()) + return; + if (player.getName().equalsIgnoreCase(args[0]) && !optionalExecutorAccount.get().hasRank(Rank.JR_DEVELOPER) && args.length >= 2) { + command.getSender().sendMessage(Style.error("Bad Sport","You cannot punish yourself!")); + return; + } + accountManager.lookup(args[0], account -> { + if (account == null) { + player.sendMessage(Style.invalidAccount("Bad Sport", args[0])); + return; + } + if (account.hasRank(Rank.HELPER) && !optionalExecutorAccount.get().hasRank(Rank.JR_DEVELOPER) && args.length >= 2) { + command.getSender().sendMessage(Style.error("Bad Sport","You can not punish other staff!")); + return; + } + new BadSportMenu(player, account, args.length >= 2 ? Arrays.stream(args).skip(1).collect(Collectors.joining(" ")) : "", false).open(); + }); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/badSportSystem/menu/BadSportMenu.java b/core/src/main/java/zone/themcgamer/core/badSportSystem/menu/BadSportMenu.java new file mode 100644 index 0000000..197a8da --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/badSportSystem/menu/BadSportMenu.java @@ -0,0 +1,219 @@ +package zone.themcgamer.core.badSportSystem.menu; + +import com.cryptomorin.xseries.XMaterial; +import org.bukkit.ChatColor; +import org.bukkit.entity.Player; +import zone.themcgamer.common.TimeUtils; +import zone.themcgamer.common.Tuple; +import zone.themcgamer.core.account.Account; +import zone.themcgamer.core.account.AccountManager; +import zone.themcgamer.core.badSportSystem.*; +import zone.themcgamer.core.common.ItemBuilder; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.core.common.menu.Button; +import zone.themcgamer.core.common.menu.Menu; +import zone.themcgamer.core.common.menu.MenuType; +import zone.themcgamer.core.module.Module; +import zone.themcgamer.data.Rank; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * @author Braydon + */ +public class BadSportMenu extends Menu { + private static final String GUIDELINES_NODE = "Refer to the guidelines for info."; + + private final Account target; + private String reason; + private final boolean history, silent; + + public BadSportMenu(Player player, Account target, String reason, boolean history) { + super(player, "BadSport | " + target.getName(), 6, MenuType.CHEST); + this.target = target; + this.reason = reason.replace("-s", ""); + if (this.reason.isEmpty()) + history = true; + this.history = history; + silent = history || reason.toLowerCase().contains("-s"); + } + + @Override + protected void onOpen() { + BadSportSystem badSportSystem = Module.getModule(BadSportSystem.class); + if (badSportSystem == null) + return; + Optional optionalBadSportClient = badSportSystem.lookup(target.getUuid()); + if (!optionalBadSportClient.isPresent()) + return; + BadSportClient badSportClient = optionalBadSportClient.get(); + + List punishments = new ArrayList<>(badSportClient.getPunishments()); + punishments.sort((a, b) -> Long.compare(b.getTimeIssued(), a.getTimeIssued())); + + // History Menu + if (history) { + int column = 1; + int slot = 1; + for (Punishment punishment : punishments) { + set(column, slot++, getPunishmentButton(punishment)); + if (get(column, 7) != null) { + column++; + slot = 1; + } + } + if (!reason.trim().isEmpty()) { + set(5, 4, new Button(new ItemBuilder(XMaterial.RED_BED) + .setName("§c« Go Back").toItemStack(), event -> new BadSportMenu(player, target, reason, false).open())); + } + return; + } + Optional optionalAccount = AccountManager.fromCache(player.getUniqueId()); + if (!optionalAccount.isPresent()) + return; + Account staffAccount = optionalAccount.get(); + + // Player Head + set(0, 4, new Button(new ItemBuilder(XMaterial.PLAYER_HEAD) + .setSkullOwner(target.getName()) + .setName("§a§l" + target.getName()).setLore( + "§fReason: §7" + reason + ).toItemStack())); + + // Left Buttons + set(1, 0, new Button(new ItemBuilder(XMaterial.LEATHER_BOOTS) + .setName("§a§lKick").setLore("§7" + GUIDELINES_NODE).toItemStack(), event -> { + close(); + punish(PunishmentCategory.KICK, PunishmentOffense.KICK, 1, -1L); + })); + set(2, 0, new Button(new ItemBuilder(XMaterial.PAPER) + .setName("§a§lWarning").setLore("§7" + GUIDELINES_NODE).toItemStack(), event -> { + close(); + punish(PunishmentCategory.WARN, PunishmentOffense.WARNING, 1, -1L); + })); + if (staffAccount.hasRank(Rank.MODERATOR)) { + set(3, 0, new Button(new ItemBuilder(XMaterial.WRITABLE_BOOK) + .setName("§a§lPermanent Mute").setLore("§7" + GUIDELINES_NODE).toItemStack(), event -> { + close(); + punish(PunishmentCategory.MUTE, PunishmentOffense.PERMANENT_MUTE, 1, -1L); + })); + } + if (staffAccount.hasRank(Rank.ADMIN)) { + set(4, 0, new Button(new ItemBuilder(XMaterial.REDSTONE_BLOCK) + .setName("§a§lPermanent Ban").setLore("§7" + GUIDELINES_NODE).toItemStack(), event -> { + close(); + punish(PunishmentCategory.BAN, PunishmentOffense.PERMANENT_BAN, 1, -1L); + })); + } + + // Offenses and Severities + int slot = 2; + for (PunishmentOffense offense : PunishmentOffense.values()) { + if (offense.getSeverities() <= 0) + continue; + set(1, slot, new Button(new ItemBuilder(offense.getIcon(), 1, offense.getData()) + .setName("§a§l" + offense.getName()).toItemStack())); + int column = 2; + + for (int severity = 1; severity <= offense.getSeverities(); severity++) { + if (severity == 2 && !staffAccount.hasRank(Rank.MODERATOR)) + continue; + if (severity == 3 && !staffAccount.hasRank(Rank.ADMIN)) + continue; + XMaterial icon = XMaterial.GREEN_STAINED_GLASS_PANE; + ChatColor color = ChatColor.GREEN; + if (severity == 2) { + icon = XMaterial.YELLOW_STAINED_GLASS_PANE; + color = ChatColor.YELLOW; + } else if (severity == 3) { + icon = XMaterial.RED_STAINED_GLASS_PANE; + color = ChatColor.RED; + } + Tuple durationTuple = offense.calculatePunishmentDuration(badSportClient, severity); + PunishmentCategory category = durationTuple.getLeft(); + long duration = durationTuple.getRight(); + int finalSeverity = severity; + set(column++, slot, new Button(new ItemBuilder(icon, severity) + .setName(color.toString() + "§lSeverity " + severity).setLore( + "§fPast Offenses: §e" + badSportClient.getPastOffenses(offense).size(), + "§f" + category.getName() + " Duration: §e" + TimeUtils.convertString(duration), + "", + "§7" + GUIDELINES_NODE + ).toItemStack(), event -> { + close(); + punish(category, offense, finalSeverity, duration); + })); + } + slot+= 2; + } + + // Right Column History + for (int column = 0; column < Math.min(punishments.size(), punishments.size() > 6 ? 5 : 6); column++) + set(column, 8, getPunishmentButton(punishments.get(column))); + if (punishments.size() > 6) { + int extra = punishments.size() - 6; + set(5, 8, new Button(new ItemBuilder(XMaterial.OAK_SIGN) + .setName("§a§lMore History").setLore( + "", + "§7Click to view §6" + extra + " §7more punishment" + (extra == 1 ? "" : "s") + " on record" + ).toItemStack(), event -> new BadSportMenu(player, target, reason, true).open())); + } + } + + public void punish(PunishmentCategory category, PunishmentOffense offense, int severity, long duration) { + BadSportSystem badSportSystem = Module.getModule(BadSportSystem.class); + if (badSportSystem == null) { + player.sendMessage(Style.error("Bad Sport", "Cannot issue punishment!")); + throw new NullPointerException(); + } + badSportSystem.punish(target.getEncryptedIpAddress(), target.getUuid(), target.getDisplayName(), category, offense, severity, + player.getUniqueId(), player.getName(), duration, reason, silent); + } + + private Button getPunishmentButton(Punishment punishment) { + List lore = new ArrayList<>(); + lore.add("§fPunishment Type: §e" + punishment.getOffense().getName()); + lore.add("§fSeverity: §e" + punishment.getSeverity()); + lore.add("§fStaff: §e" + punishment.getStaffName()); + lore.add("§fDate: §e" + TimeUtils.when(punishment.getTimeIssued())); + if (punishment.getCategory() != PunishmentCategory.KICK && punishment.getCategory() != PunishmentCategory.WARN) + lore.add("§fLength: §e" + TimeUtils.convertString(punishment.getDuration())); + lore.add(""); + lore.add("§fReason: §e" + punishment.getReason()); + if (punishment.isActive()) { + lore.add(""); + lore.add("§fExpires On: §e" + TimeUtils.when(System.currentTimeMillis() + punishment.getRemaining()) + + " (" + (punishment.isPermanent() ? "Permanent" : TimeUtils.convertString(punishment.getRemaining())) + ")"); + } + if (punishment.wasRemoved() && !punishment.wasOverriden()) { + lore.add(""); + lore.add("§fRemove Staff: §e" + punishment.getRemoveStaffName()); + lore.add("§fRemove Reason: §e" + punishment.getRemoveReason()); + } + lore.add(""); + if (punishment.isActive()) { + lore.add("§cClick to remove"); + } else lore.add("§aClick to reapply punishment"); + return new Button(new ItemBuilder(punishment.getOffense().getIcon(), 1, punishment.getOffense().getData()) + .setGlow(punishment.isActive()) + .setName("§a§l" + punishment.getOffense().getName()) + .setLore(lore).toItemStack(), event -> { + if (punishment.getCategory() == PunishmentCategory.WARN) + return; + if (punishment.isActive()) { + close(); + BadSportSystem badSportSystem = Module.getModule(BadSportSystem.class); + if (badSportSystem != null) + badSportSystem.remove(punishment, target.getDisplayName(), player.getUniqueId(), player.getName(), reason, silent, false); + } else { + if (!reason.toLowerCase().contains("reapplied")) + reason = (reason.trim().isEmpty() ? punishment.getReason() : reason) + " - Reapplied"; + close(); + punish(punishment.getCategory(), punishment.getOffense(), punishment.getSeverity(), + (punishment.getDuration() == -1L ? -1L : punishment.getDuration() * 2)); + } + }); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/chat/ChatManager.java b/core/src/main/java/zone/themcgamer/core/chat/ChatManager.java new file mode 100644 index 0000000..9743975 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/chat/ChatManager.java @@ -0,0 +1,92 @@ +package zone.themcgamer.core.chat; + +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.ComponentBuilder; +import net.md_5.bungee.api.chat.TextComponent; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.player.AsyncPlayerChatEvent; +import org.bukkit.plugin.java.JavaPlugin; +import zone.themcgamer.core.badSportSystem.BadSportClient; +import zone.themcgamer.core.badSportSystem.BadSportSystem; +import zone.themcgamer.core.badSportSystem.Punishment; +import zone.themcgamer.core.badSportSystem.PunishmentCategory; +import zone.themcgamer.core.chat.command.ClearChatCommand; +import zone.themcgamer.core.chat.component.IChatComponent; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.core.module.Module; +import zone.themcgamer.core.module.ModuleInfo; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +/** + * @author Braydon + */ +@ModuleInfo(name = "Chat") +public class ChatManager extends Module { + private final BadSportSystem badSportSystem; + private final IChatComponent[] chatComponents; + + public ChatManager(JavaPlugin plugin, BadSportSystem badSportSystem, IChatComponent[] chatComponents) { + super(plugin); + this.badSportSystem = badSportSystem; + this.chatComponents = chatComponents; + registerCommand(new ClearChatCommand()); + + /* TODO + /chatmanager blackwords add + /chatmanager blackwords remove + /chatmanager emote add + /chatmanager emote remove + /chatmanager urls add + /chatmanager remove + */ + } + + @EventHandler + private void onChat(AsyncPlayerChatEvent event) { + Player player = event.getPlayer(); + String message = event.getMessage(); + + event.setCancelled(true); + + Optional optionalBadSportClient = badSportSystem.lookup(player.getUniqueId()); + if (optionalBadSportClient.isEmpty()) { + player.sendMessage(Style.error("Chat", "§cCannot find bad sport profile")); + return; + } + Optional optionalMute = optionalBadSportClient.get().getMute(); + if (optionalMute.isPresent()) { + player.sendMessage(Style.error("Bad Sport", PunishmentCategory.format(optionalMute.get()))); + return; + } + // TODO: 1/26/21 filter message + if (message.trim().isEmpty()) { + player.sendMessage(Style.error("Chat", "§cCannot send empty chat message")); + return; + } + if (chatComponents.length <= 0) { + player.sendMessage(Style.error("Chat", "§cCannot format chat message")); + return; + } + List components = new ArrayList<>(); + for (IChatComponent chatComponent : chatComponents) { + BaseComponent component = chatComponent.getComponent(player); + if (component == null) + continue; + components.add(component); + components.add(new TextComponent(" ")); + } + components.add(new TextComponent("§8» §7")); + components.addAll(Arrays.asList(new ComponentBuilder(message).color(ChatColor.GRAY).create())); + + BaseComponent[] baseComponents = components.toArray(new BaseComponent[0]); + for (Player online : Bukkit.getOnlinePlayers()) + online.sendMessage(baseComponents); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/chat/command/ClearChatCommand.java b/core/src/main/java/zone/themcgamer/core/chat/command/ClearChatCommand.java new file mode 100644 index 0000000..c6fda49 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/chat/command/ClearChatCommand.java @@ -0,0 +1,19 @@ +package zone.themcgamer.core.chat.command; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.data.Rank; + +public class ClearChatCommand { + @Command(name = "clearchat", aliases = { "cc" }, description = "Clear the chat", ranks = { Rank.HELPER }, playersOnly = true) + public void onCommand(CommandProvider command) { + for (Player onlinePlayer : Bukkit.getOnlinePlayers()) { + for (int i = 0; i < 150; i++) + onlinePlayer.sendMessage(" "); + onlinePlayer.sendMessage(Style.main("Chat", "The chat has been cleared by &6" + command.getPlayer().getName())); + } + } +} diff --git a/core/src/main/java/zone/themcgamer/core/chat/component/IChatComponent.java b/core/src/main/java/zone/themcgamer/core/chat/component/IChatComponent.java new file mode 100644 index 0000000..738ba59 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/chat/component/IChatComponent.java @@ -0,0 +1,11 @@ +package zone.themcgamer.core.chat.component; + +import net.md_5.bungee.api.chat.BaseComponent; +import org.bukkit.entity.Player; + +/** + * @author Braydon + */ +public interface IChatComponent { + BaseComponent getComponent(Player player); +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/chat/component/impl/BasicNameComponent.java b/core/src/main/java/zone/themcgamer/core/chat/component/impl/BasicNameComponent.java new file mode 100644 index 0000000..16a85fa --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/chat/component/impl/BasicNameComponent.java @@ -0,0 +1,22 @@ +package zone.themcgamer.core.chat.component.impl; + +import net.md_5.bungee.api.chat.*; +import org.bukkit.entity.Player; +import zone.themcgamer.common.MiscUtils; +import zone.themcgamer.core.chat.component.IChatComponent; +import zone.themcgamer.core.common.Style; + +/** + * @author Braydon + */ +public class BasicNameComponent implements IChatComponent { + @Override + public BaseComponent getComponent(Player player) { + ComponentBuilder componentBuilder = new ComponentBuilder("§7" + player.getName()); + componentBuilder.event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder(MiscUtils.arrayToString( + Style.color("&7Player: &6" + player.getName()), + Style.color("&aClick to message me!"))).create())); + componentBuilder.event(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/msg " + player.getName() + " (message)")).create(); + return new TextComponent(componentBuilder.create()); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/chat/component/impl/BasicRankComponent.java b/core/src/main/java/zone/themcgamer/core/chat/component/impl/BasicRankComponent.java new file mode 100644 index 0000000..98a9f35 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/chat/component/impl/BasicRankComponent.java @@ -0,0 +1,32 @@ +package zone.themcgamer.core.chat.component.impl; + +import net.md_5.bungee.api.chat.*; +import org.bukkit.entity.Player; +import zone.themcgamer.common.MiscUtils; +import zone.themcgamer.core.account.Account; +import zone.themcgamer.core.account.AccountManager; +import zone.themcgamer.core.chat.component.IChatComponent; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.data.Rank; + +import java.util.Optional; + +/** + * @author Braydon + */ +public class BasicRankComponent implements IChatComponent { + @Override + public BaseComponent getComponent(Player player) { + Optional optionalAccount = AccountManager.fromCache(player.getUniqueId()); + Account account; + if (optionalAccount.isEmpty() || ((account = optionalAccount.get()).getPrimaryRank() == Rank.DEFAULT)) + return null; + ComponentBuilder componentBuilder = new ComponentBuilder(account.getPrimaryRank().getPrefix()); + componentBuilder.event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder(MiscUtils.arrayToString( + Style.color("&7This is &b" + account.getPrimaryRank().getDisplayName() + " &7rank"), + Style.color("&7Do you also want to stand out in the &achat&7?"), + Style.color("&e&lClick Me &7to donate and support the server!"))).create())); + componentBuilder.event(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/store")).create(); + return new TextComponent(componentBuilder.create()); + } +} diff --git a/core/src/main/java/zone/themcgamer/core/command/BukkitCommand.java b/core/src/main/java/zone/themcgamer/core/command/BukkitCommand.java new file mode 100644 index 0000000..2ff8cf7 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/command/BukkitCommand.java @@ -0,0 +1,70 @@ +package zone.themcgamer.core.command; + +import org.apache.commons.lang.Validate; +import org.bukkit.command.Command; +import org.bukkit.command.*; +import org.bukkit.plugin.Plugin; + +import java.util.List; + +/** + * This class is an override for the default {@link org.bukkit.command.defaults.BukkitCommand} + * class + * @author Braydon + */ +public class BukkitCommand extends Command { + private final Plugin plugin; + private final CommandExecutor executor; + protected BukkitCompleter completer; + + public BukkitCommand(String label, Plugin plugin, CommandExecutor executor) { + super(label); + this.plugin = plugin; + this.executor = executor; + usageMessage = ""; + } + + @Override + public boolean execute(CommandSender sender, String label, String[] args) { + if (!plugin.isEnabled()) + return false; + if (!testPermission(sender)) + return true; + boolean success; + try { + success = executor.onCommand(sender, this, label, args); + } catch (Exception ex) { + throw new CommandException("Unhandled exception executing command '" + label + "' in plugin " + plugin.getDescription().getFullName(), ex); + } + if (!success && usageMessage.length() > 0) { + for (String line : usageMessage.replace("", label).split("\n")) { + sender.sendMessage(line); + } + } + return success; + } + + @Override + public List tabComplete(CommandSender sender, String alias, String[] args) throws CommandException, IllegalArgumentException { + Validate.notNull(sender, "Sender cannot be null"); + Validate.notNull(args, "Arguments cannot be null"); + Validate.notNull(alias, "Alias cannot be null"); + List completions = null; + try { + if (completer != null) + completions = completer.onTabComplete(sender, this, alias, args); + if (completions == null && executor instanceof TabCompleter) + completions = ((TabCompleter) executor).onTabComplete(sender, this, alias, args); + } catch (Exception ex) { + StringBuilder message = new StringBuilder(); + message.append("Unhandled exception during tab completion for command '/").append(alias).append(' '); + for (String arg : args) + message.append(arg).append(" "); + message.deleteCharAt(message.length() - 1).append("' in plugin ").append(plugin.getDescription().getFullName()); + throw new CommandException(message.toString(), ex); + } + if (completions == null) + return super.tabComplete(sender, alias, args); + return completions; + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/command/BukkitCompleter.java b/core/src/main/java/zone/themcgamer/core/command/BukkitCompleter.java new file mode 100644 index 0000000..42a40f0 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/command/BukkitCompleter.java @@ -0,0 +1,51 @@ +package zone.themcgamer.core.command; + +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.AbstractMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * This class is an override for the default class + * @author Braydon + */ +public class BukkitCompleter implements TabCompleter { + private final Map> completers = new HashMap<>(); + + @Override @SuppressWarnings("unchecked") + public List onTabComplete(CommandSender sender, Command command, String label, String[] args) { + for (int i = args.length; i >= 0; i--) { + StringBuilder builder = new StringBuilder(); + builder.append(label.toLowerCase()); + for (int j = 0; j < i; j++) { + if (!args[j].equals("") && !args[j].equals(" ")) { + builder.append(".").append(args[j].toLowerCase()); + } + } + String commandLabel = builder.toString(); + if (completers.containsKey(commandLabel)) { + Map.Entry entry = completers.get(commandLabel); + try { + List completions = (List) entry.getKey().invoke(entry.getValue(), + new CommandProvider(sender, command, commandLabel, args, commandLabel.split("\\.").length - 1)); + if (completions == null || (completions.isEmpty())) + return null; + return completions; + } catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException ex) { + ex.printStackTrace(); + } + } + } + return null; + } + + public void addCompleter(String label, Method method, Object object) { + completers.put(label, new AbstractMap.SimpleEntry<>(method, object)); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/command/Command.java b/core/src/main/java/zone/themcgamer/core/command/Command.java new file mode 100644 index 0000000..ea4f659 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/command/Command.java @@ -0,0 +1,30 @@ +package zone.themcgamer.core.command; + +import zone.themcgamer.data.Rank; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation holds information for a command + * @author Braydon + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Command { + String name(); + + String[] aliases() default {}; + + String usage() default ""; + + String description() default ""; + + Rank[] ranks() default { Rank.DEFAULT }; + + boolean terminalOnly() default false; + + boolean playersOnly() default false; +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/command/CommandManager.java b/core/src/main/java/zone/themcgamer/core/command/CommandManager.java new file mode 100644 index 0000000..1944b49 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/command/CommandManager.java @@ -0,0 +1,331 @@ +package zone.themcgamer.core.command; + +import lombok.Getter; +import org.bukkit.Bukkit; +import org.bukkit.command.*; +import org.bukkit.entity.Player; +import org.bukkit.help.GenericCommandHelpTopic; +import org.bukkit.help.HelpTopic; +import org.bukkit.help.HelpTopicComparator; +import org.bukkit.help.IndexHelpTopic; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.SimplePluginManager; +import org.bukkit.plugin.java.JavaPlugin; +import zone.themcgamer.common.TriTuple; +import zone.themcgamer.core.account.Account; +import zone.themcgamer.core.account.AccountManager; +import zone.themcgamer.core.command.help.HelpCommand; +import zone.themcgamer.core.command.impl.DiscordCommand; +import zone.themcgamer.core.command.impl.RulesCommand; +import zone.themcgamer.core.command.impl.StoreCommand; +import zone.themcgamer.core.command.impl.StressTestCommand; +import zone.themcgamer.core.command.impl.essentials.GameModeCommand; +import zone.themcgamer.core.command.impl.essentials.TeleportCommand; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.core.module.Module; +import zone.themcgamer.core.module.ModuleInfo; +import zone.themcgamer.data.Rank; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.*; + +/** + * @author Braydon + */ +@ModuleInfo(name = "Command Manager") +public class CommandManager extends Module implements CommandExecutor { + /** + * An array of default commands to disable on the server. "Default" commands meaning + * Minecraft, Bukkit, and Spigot commands that come with the server jar + */ + private static final String[] DISABLED_COMMANDS = new String[] { + "op", + "deop", + "kick", + "ban", + "banlist", + "pardon", + "pardon-ip", + "reload", + "rl", + "stop", + "restart", + "me", + "say", + "about", + "ver", + "version", + "icanhasbukkit", + "trigger", + "ban-ip", + "execute", + "function", + "spigot", + "plugins", + "pl", + "stats" + }; + + private CommandMap commandMap; + private Map knownCommands; + @Getter private final Map> commands = new HashMap<>(); + private final Map> registrationQueue = new HashMap<>(); + + public CommandManager(JavaPlugin plugin) { + super(plugin); + SimplePluginManager simplePluginManager = (SimplePluginManager) Bukkit.getPluginManager(); + try { + Field field = SimplePluginManager.class.getDeclaredField("commandMap"); + field.setAccessible(true); + commandMap = (CommandMap) field.get(simplePluginManager); + + Field knownCommandsField = commandMap.getClass().getDeclaredField("knownCommands"); + knownCommandsField.setAccessible(true); + knownCommands = (Map) knownCommandsField.get(commandMap); + } catch (IllegalArgumentException | SecurityException | IllegalAccessException | NoSuchFieldException ex) { + ex.printStackTrace(); + } + + // Registering default commands + registerCommand(plugin, new StressTestCommand(plugin)); + registerCommand(plugin, new RulesCommand()); + registerCommand(plugin, new TeleportCommand()); + registerCommand(plugin, new GameModeCommand()); + registerCommand(plugin, new zone.themcgamer.core.command.impl.HelpCommand(this)); + registerCommand(plugin, new DiscordCommand()); + registerCommand(plugin, new StoreCommand()); + + Bukkit.getScheduler().scheduleSyncDelayedTask(plugin, () -> { + for (String disabledCommand : DISABLED_COMMANDS) { + String[] prefixes = new String[] { "minecraft", "bukkit", "spigot" }; + List commands = new ArrayList<>(Collections.singletonList(disabledCommand.toLowerCase())); + for (String prefix : prefixes) + commands.add(prefix + ":" + disabledCommand.toLowerCase()); + for (String command : commands) + unregisterCommand(command); + } + for (Map.Entry> entry : registrationQueue.entrySet()) { + for (Object object : entry.getValue()) { + for (Method method : object.getClass().getMethods()) { + if (method.getAnnotation(Command.class) != null) { + Command command = method.getAnnotation(Command.class); + if (method.getParameterTypes().length != 1 || !method.getParameterTypes()[0].equals(CommandProvider.class)) { + plugin.getLogger().warning("Unable to register command " + method.getName() + ". Unexpected method arguments"); + continue; + } + List knownAliases = new ArrayList<>(Collections.singletonList(command.name().toLowerCase())); + for (String alias : command.aliases()) + knownAliases.add(alias.toLowerCase()); + for (String knownAlias : knownAliases) { + if (knownCommands.containsKey(knownAlias)) + unregisterCommand(knownAlias); + registerCommand(plugin, command, method, object, knownAlias); + } + } else if (method.getAnnotation(TabComplete.class) != null) { + TabComplete tabComplete = method.getAnnotation(TabComplete.class); + if (method.getParameterTypes().length != 1 || !method.getParameterTypes()[0].equals(CommandProvider.class)) { + plugin.getLogger().warning("Unable to register tab completer " + method.getName() + ". Unexpected method arguments"); + continue; + } + if (!method.getReturnType().equals(List.class)) { + plugin.getLogger().warning("Unable to register tab completer " + method.getName() + ". Unexpected return type"); + continue; + } + registerTabComplete(plugin, tabComplete.name(), method, object); + for (String alias : tabComplete.aliases()) + registerTabComplete(plugin, alias, method, object); + } + } + } + } + registerHelp(plugin); + }, 13L); + } + + /** + * Register a command + * @param plugin - The owner of the command + * @param object - The instance of the command class + */ + public void registerCommand(Plugin plugin, Object object) { + if (plugin == null || object == null) + throw new IllegalArgumentException("Plugin or command object provided is null"); + List queue = registrationQueue.getOrDefault(plugin, new ArrayList<>()); + queue.add(object); + registrationQueue.put(plugin, queue); + } + + /** + * Add all of the currently registered commands to the help menu + * @param plugin - The plugin owner of the commands to add to + * the help menu + */ + private void registerHelp(Plugin plugin) { + try { + Set help = new TreeSet<>(HelpTopicComparator.helpTopicComparatorInstance()); + for (String alias : commands.keySet()) { + if (!alias.contains(".")) { + org.bukkit.command.Command bukkitCommand = commandMap.getCommand(alias); + if (bukkitCommand == null) + bukkitCommand = new BukkitCommand(alias, plugin, this); + HelpTopic topic = new GenericCommandHelpTopic(bukkitCommand); + help.add(topic); + } + } + IndexHelpTopic topic = new IndexHelpTopic(plugin.getName(), "All commands for " + plugin.getName(), null, help, + "Below is a list of all " + plugin.getName() + " commands:"); + Bukkit.getServer().getHelpMap().addTopic(topic); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + + public void unregisterCommand(String alias) { + org.bukkit.command.Command bukkitCommand = knownCommands.get(alias); + if (bukkitCommand == null || (bukkitCommand instanceof BukkitCommand)) + return; + log("Unregistered \"/" + alias + "\""); + knownCommands.remove(alias); + bukkitCommand.unregister(commandMap); + } + + /** + * Unregister all commands + */ + public void cleanup() { + for (String alias : commands.keySet()) { + org.bukkit.command.Command bukkitCommand = knownCommands.remove(alias.toLowerCase()); + if (bukkitCommand == null) + continue; + bukkitCommand.unregister(commandMap); + } + } + + private void registerCommand(Plugin plugin, Command commandAnnotation, Method method, Object object, String rawLabel) { + long time = System.nanoTime(); + commands.put(rawLabel.toLowerCase(), new TriTuple<>(method, object, time)); + commands.put(plugin.getName() + ':' + rawLabel.toLowerCase(), new TriTuple<>(method, object, time)); + String label = rawLabel.replace(".", ",").split(",")[0].toLowerCase(); + if (commandMap.getCommand(label) == null) { + org.bukkit.command.Command cmd = new BukkitCommand(label, plugin, this); + commandMap.register(plugin.getName(), cmd); + } + if (!commandAnnotation.usage().isEmpty() && label.equals(rawLabel)) + commandMap.getCommand(label).setUsage(commandAnnotation.usage()); + if (!commandAnnotation.description().isEmpty() && label.equals(rawLabel)) + commandMap.getCommand(label).setDescription(commandAnnotation.description()); + } + + private void registerTabComplete(Plugin plugin, String rawLabel, Method method, Object object) { + String label = rawLabel.replace(".", ",").split(",")[0].toLowerCase(); + if (commandMap.getCommand(label) == null) { + org.bukkit.command.Command command = new BukkitCommand(label, plugin, this); + commandMap.register(plugin.getName(), command); + } + if (commandMap.getCommand(label) instanceof BukkitCommand) { + BukkitCommand command = (BukkitCommand) commandMap.getCommand(label); + if (command.completer == null) + command.completer = new BukkitCompleter(); + command.completer.addCompleter(rawLabel, method, object); + } else if (commandMap.getCommand(label) instanceof PluginCommand) { + try { + Object command = commandMap.getCommand(label); + Field field = command.getClass().getDeclaredField("completer"); + field.setAccessible(true); + if (field.get(command) == null) { + BukkitCompleter completer = new BukkitCompleter(); + completer.addCompleter(rawLabel, method, object); + field.set(command, completer); + } else if (field.get(command) instanceof BukkitCompleter) { + BukkitCompleter completer = (BukkitCompleter) field.get(command); + completer.addCompleter(rawLabel, method, object); + } else { + plugin.getLogger().warning("Unable to register tab completer " + method.getName() + + ". A tab completer is already registered for that command!"); + } + } catch (Exception ex) { + ex.printStackTrace(); + } + } + } + + @Override + public boolean onCommand(CommandSender sender, org.bukkit.command.Command bukkitCommand, String rawLabel, String[] args) { + if (rawLabel.contains(":")) { + String[] split = rawLabel.split(":"); + for (Plugin plugin : Bukkit.getPluginManager().getPlugins()) { + if (split[0].equalsIgnoreCase(plugin.getName())) { + rawLabel = split[1]; + break; + } + } + } + for (int i = args.length; i >= 0; i--) { + StringBuilder builder = new StringBuilder(); + builder.append(rawLabel.toLowerCase()); + for (int j = 0; j < i; j++) + builder.append(".").append(args[j].toLowerCase()); + String label = builder.toString(); + if (commands.containsKey(label)) { + Method method = commands.get(label).getLeft(); + Object object = commands.get(label).getMiddle(); + Command command = method.getAnnotation(Command.class); + if (command.terminalOnly() && sender instanceof Player) + sender.sendMessage(Style.color("&cThis command can only be executed via the terminal.")); + else if (command.playersOnly() && sender instanceof ConsoleCommandSender) + sender.sendMessage(Style.color("&cThis command can only be executed from in-game.")); + else { + if (sender instanceof Player) { + Optional optionalAccount = AccountManager.fromCache(((Player) sender).getUniqueId()); + if (!optionalAccount.isPresent()) { + sender.sendMessage(Style.error("Account","&cCannot fetch account")); + return true; + } + if (Arrays.stream(command.ranks()).anyMatch(rank -> !optionalAccount.get().hasRank(rank))) { + Optional rank = Arrays.stream(command.ranks()).findFirst(); + rank.ifPresent(value -> sender.sendMessage(Style.rankRequired(rank.get()))); + return true; + } + } + if (object instanceof HelpCommand) { + Map childCommandsMap = new HashMap<>(); + for (Map.Entry> entry : commands.entrySet()) { + String s = entry.getKey(); + if (s.contains(".") && (s.split("\\.")[0].equalsIgnoreCase(command.name()))) { + TriTuple triTuple = entry.getValue(); + childCommandsMap.put(triTuple.getLeft().getAnnotation(Command.class), triTuple.getRight()); + } + } + List childCommands = new ArrayList<>(childCommandsMap.keySet()); + childCommands.sort(Comparator.comparingLong(childCommandsMap::get)); + HelpCommand helpCommand = (HelpCommand) object; + + int page = 1; + if (args.length >= 1) { + try { + page = Integer.parseInt(args[0]); + } catch (NumberFormatException ignored) { + page = -1; + } + } + if (helpCommand.sendHelp(sender, label, command, childCommands, page) == HelpCommand.HelpResponse.INVALID_PAGE) + sender.sendMessage(Style.color("&cInvalid page.")); + return true; + } + try { + method.invoke(object, new CommandProvider(sender, bukkitCommand, rawLabel, args, + label.split("\\.").length - 1)); + } catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException ex) { + sender.sendMessage(Style.color("&cAn error has occurred whilst executing the command, this exception has been logged! §f" + ex.getLocalizedMessage())); + ex.printStackTrace(); + } + } + return true; + } + } + sender.sendMessage(Style.color("&cFailed to handle command")); + return true; + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/command/CommandProvider.java b/core/src/main/java/zone/themcgamer/core/command/CommandProvider.java new file mode 100644 index 0000000..0aed3ea --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/command/CommandProvider.java @@ -0,0 +1,42 @@ +package zone.themcgamer.core.command; + +import lombok.Getter; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +/** + * This class is constructed everytime a command is executed. + * It holds information such as the sender, label, and arguments + * @author Braydon + */ +@Getter +public class CommandProvider { + private final CommandSender sender; + private final org.bukkit.command.Command bukkitCommand; + private final String label; + private final String[] args; + + public CommandProvider(CommandSender sender, org.bukkit.command.Command bukkitCommand, String label, String[] args, int subCommands) { + this.sender = sender; + this.bukkitCommand = bukkitCommand; + + StringBuilder builder = new StringBuilder(); + builder.append(label); + for (int i = 0; i < subCommands; i++) + builder.append(".").append(args[i]); + this.label = builder.toString(); + + String[] newArgs = new String[args.length - subCommands]; + if (args.length - subCommands >= 0) + System.arraycopy(args, subCommands, newArgs, 0, args.length - subCommands); + this.args = newArgs; + } + + public boolean isPlayer() { + return getPlayer() != null; + } + + public Player getPlayer() { + return sender instanceof Player ? (Player) sender : null; + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/command/TabComplete.java b/core/src/main/java/zone/themcgamer/core/command/TabComplete.java new file mode 100644 index 0000000..8b3b1c5 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/command/TabComplete.java @@ -0,0 +1,18 @@ +package zone.themcgamer.core.command; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation holds information for a command tab completer + * @author Braydon + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface TabComplete { + String name(); + + String[] aliases() default {}; +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/command/help/HelpColorScheme.java b/core/src/main/java/zone/themcgamer/core/command/help/HelpColorScheme.java new file mode 100644 index 0000000..f180093 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/command/help/HelpColorScheme.java @@ -0,0 +1,13 @@ +package zone.themcgamer.core.command.help; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * This class holds information for a help menu color scheme + * @author Braydon + */ +@AllArgsConstructor @Getter +public class HelpColorScheme { + private final String header, primaryColor, secondaryColor; +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/command/help/HelpCommand.java b/core/src/main/java/zone/themcgamer/core/command/help/HelpCommand.java new file mode 100644 index 0000000..2ed9178 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/command/help/HelpCommand.java @@ -0,0 +1,76 @@ +package zone.themcgamer.core.command.help; + +import org.apache.commons.lang.WordUtils; +import org.bukkit.command.CommandSender; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.common.PageBuilder; +import zone.themcgamer.data.Rank; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * This class serves the purpose of sending the help menu + * to the executor. The {@code sendHelp} method can be overridden + * inside of a command class so that command can have it's own help + * menu instead of the default one + * @author Braydon + */ +public class HelpCommand { + /** + * Get the color scheme for the help menu + * @return the color scheme + */ + public HelpColorScheme getColorScheme() { + return new HelpColorScheme(null, "§6", "§e"); + } + + /** + * Send the help menu to the provided sender + * @param sender - The sender to send the help menu to + * @param label - The command label + * @param parent - The parent command + * @param children - The child commands + * @param page - The page to display + * @return the response + */ + public HelpResponse sendHelp(CommandSender sender, String label, Command parent, List children, int page) { + HelpColorScheme colorScheme = getColorScheme(); + String header = colorScheme.getHeader(); + if (header == null) + header = WordUtils.capitalize(parent.name().toLowerCase()); + List commands = new ArrayList<>(Collections.singletonList(parent)); + commands.addAll(children); + PageBuilder pageBuilder = new PageBuilder<>(commands, (entry, command) -> { + String usage = parent.usage().isEmpty() ? label + " [page]" : parent.usage(); + boolean isParent = parent.equals(command); + if (!isParent) { + String[] split = command.name().split("\\."); + usage = split[split.length - 1] + (command.usage().isEmpty() ? "" : " " + command.usage()); + } + String descriptionExtension = ""; + if (!command.description().isEmpty() || isParent) + descriptionExtension = " §8- §7" + (!command.description().isEmpty() ? command.description() : "Show's this menu"); + String rankExtension = ""; + Rank rank = command.ranks()[0]; + if (rank != Rank.DEFAULT) + rankExtension = " " + rank.getColor() + rank.getDisplayName(); + sender.sendMessage(" §7- §" + (isParent ? "f" : "7") + "/" + (isParent ? "" : label + " §f") + usage + descriptionExtension + rankExtension); + }).resultsPerPage(5); + int maxPage = pageBuilder.getMaxPages(); + if (page <= 0 || page > maxPage) + return HelpResponse.INVALID_PAGE; + sender.sendMessage(""); + sender.sendMessage(colorScheme.getPrimaryColor() + "§l" + header + " " + colorScheme.getSecondaryColor() + "Help §7(Page " + page + " / " + maxPage + ")"); + sender.sendMessage(colorScheme.getSecondaryColor() + "<>§7 = required, " + colorScheme.getSecondaryColor() + "[]§7 = optional"); + sender.sendMessage(""); + pageBuilder.send(sender, page); + sender.sendMessage(""); + return HelpResponse.SUCCESS; + } + + public enum HelpResponse { + INVALID_PAGE, SUCCESS + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/command/impl/DiscordCommand.java b/core/src/main/java/zone/themcgamer/core/command/impl/DiscordCommand.java new file mode 100644 index 0000000..bf07be8 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/command/impl/DiscordCommand.java @@ -0,0 +1,13 @@ +package zone.themcgamer.core.command.impl; + +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.core.common.Style; + +public class DiscordCommand { + @Command(name = "discord", description = "A discord full with players to chat with", playersOnly = true) + public void onCommand(CommandProvider command) { + command.getPlayer().sendMessage(Style.main("Discord", "&e&lClick&7 this link to join our discord: &9discord.mcgamerzone.net")); + //TODO this will also include the linking system. To generate the token. + } +} diff --git a/core/src/main/java/zone/themcgamer/core/command/impl/HelpCommand.java b/core/src/main/java/zone/themcgamer/core/command/impl/HelpCommand.java new file mode 100644 index 0000000..6c85d09 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/command/impl/HelpCommand.java @@ -0,0 +1,79 @@ +package zone.themcgamer.core.command.impl; + +import lombok.AllArgsConstructor; +import net.md_5.bungee.api.chat.ClickEvent; +import net.md_5.bungee.api.chat.ComponentBuilder; +import net.md_5.bungee.api.chat.HoverEvent; +import org.bukkit.entity.Player; +import zone.themcgamer.common.MiscUtils; +import zone.themcgamer.common.TriTuple; +import zone.themcgamer.core.account.Account; +import zone.themcgamer.core.account.AccountManager; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandManager; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.core.common.PageBuilder; +import zone.themcgamer.core.common.Style; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +@AllArgsConstructor +public class HelpCommand { + private final CommandManager commandManager; + + @Command(name = "help", aliases = { "?", "h" }, description = "View the help menu", playersOnly = true) + public void onCommand(CommandProvider command) { + Player player = command.getPlayer(); + String[] args = command.getArgs(); + int page = 1; + if (args.length >= 1) { + try { + page = Integer.parseInt(args[0]); + } catch (NumberFormatException ignored) { + page = -1; + } + } + Optional optionalAccount = AccountManager.fromCache(player.getUniqueId()); + if (optionalAccount.isEmpty()) + return; + Account account = optionalAccount.get(); + + Map commandsMap = new HashMap<>(); + for (TriTuple triTuple : commandManager.getCommands().values()) { + Method method = triTuple.getLeft(); + if (!method.isAnnotationPresent(Command.class)) + continue; + Command annotation = method.getAnnotation(Command.class); + if (!account.hasRank(annotation.ranks()[0])) + continue; + String commandName = annotation.name(); + if (commandName.contains(".")) + continue; + commandsMap.put(commandName, annotation); + } + PageBuilder pageBuilder = new PageBuilder<>(new ArrayList<>(commandsMap.entrySet()), (place, entry) -> { + player.sendMessage(new ComponentBuilder(Style.color("&e ▸ &b/" + entry.getKey() + " &8- &7" + entry.getValue().description())) + .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder(Style.color(MiscUtils.arrayToString( + "&7Aliases: &b" + (entry.getValue().aliases().length == 0 ? "None" : String.join("&6, &b", entry.getValue().aliases())), + "&aClick to execute this command" + ))).create())) + .event(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/" + entry.getKey() + " " + entry.getValue().usage())) + .create()); + }); + int maxPage = pageBuilder.getMaxPages(); + if (page <= 0 || page > maxPage) { + player.sendMessage("§cPage out of bounds. There " + (maxPage == 1 ? "is" : "are") + " only §l" + maxPage + " §cpage" + (maxPage == 1 ? "" : "s") + "."); + return; + } + player.sendMessage(""); + player.sendMessage("§2§lMc§6§lGamer§c§lZone §7(Page " + page + " / " + maxPage + ")"); + player.sendMessage("§6<>§7 = required, §6[]§7 = optional"); + player.sendMessage(""); + pageBuilder.send(player, page); + player.sendMessage(""); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/command/impl/RulesCommand.java b/core/src/main/java/zone/themcgamer/core/command/impl/RulesCommand.java new file mode 100644 index 0000000..36d450e --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/command/impl/RulesCommand.java @@ -0,0 +1,13 @@ +package zone.themcgamer.core.command.impl; + +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.core.common.Style; + +public class RulesCommand { + @Command(name = "rules", description = "View the rules", playersOnly = true) + public void onCommand(CommandProvider command) { + command.getPlayer().sendMessage(Style.main("Rules", "In order to play you will have to follow our rules.\n" + + "You can find our rules at: &bhttps://mcgamerzone.net/rules")); + } +} diff --git a/core/src/main/java/zone/themcgamer/core/command/impl/StoreCommand.java b/core/src/main/java/zone/themcgamer/core/command/impl/StoreCommand.java new file mode 100644 index 0000000..234b86a --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/command/impl/StoreCommand.java @@ -0,0 +1,12 @@ +package zone.themcgamer.core.command.impl; + +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.core.common.Style; + +public class StoreCommand { + @Command(name = "store", aliases = "buy", description = "Visit our webstore and support us", playersOnly = true) + public void onCommand(CommandProvider command) { + command.getPlayer().sendMessage(Style.main("Store", "&7Buy &aRanks&7, &eBundles&7, &dBoosters &7and &9&lmore&7 at: &dstore.mcgamerzone.net")); + } +} diff --git a/core/src/main/java/zone/themcgamer/core/command/impl/StressTestCommand.java b/core/src/main/java/zone/themcgamer/core/command/impl/StressTestCommand.java new file mode 100644 index 0000000..0e646c0 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/command/impl/StressTestCommand.java @@ -0,0 +1,63 @@ +package zone.themcgamer.core.command.impl; + +import lombok.RequiredArgsConstructor; +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.scheduler.BukkitTask; +import zone.themcgamer.common.HashUtils; +import zone.themcgamer.common.RandomUtils; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.data.Rank; + +import java.util.UUID; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.IntStream; + +/** + * @author Braydon + */ +@RequiredArgsConstructor +public class StressTestCommand { + private final JavaPlugin plugin; + private BukkitTask task; + + @Command(name = "stresstest", ranks = { Rank.DEVELOPER }, description = "Stress test the server") + public void onCommand(CommandProvider command) { + CommandSender sender = command.getSender(); + if (task != null) { + task.cancel(); + task = null; + sender.sendMessage(Style.main("Stress Test", "§cStopped task...")); + return; + } + sender.sendMessage(Style.main("Stress Test", "Task started!")); + task = Bukkit.getScheduler().runTaskTimer(plugin, () -> { + AtomicReference sqrt = new AtomicReference<>((double) 2); + AtomicInteger count = new AtomicInteger(); + (ThreadLocalRandom.current().nextBoolean() ? IntStream.range(0, 18000).parallel() : IntStream.range(0, 8000)).forEach(i -> { + sqrt.set(Math.sqrt(sqrt.get() * i) / 2.); + + try { + Class.forName("net.minecraft.server.v1_8_R3.MinecraftServer").getDeclaredMethod("a", String.class); + } catch (Exception ignored) {} + + for (int j = 0; j < ThreadLocalRandom.current().nextInt(3); j++) + HashUtils.encryptSha256(UUID.randomUUID().toString() + UUID.randomUUID().toString()); + count.incrementAndGet(); + }); + if (RandomUtils.randomInt(0, 50) > 43) { + try { + System.out.println("Sleeping..."); + Thread.sleep(30L); + } catch (InterruptedException ex) { + ex.printStackTrace(); + } + } + }, 1L, 1L); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/command/impl/essentials/GameModeCommand.java b/core/src/main/java/zone/themcgamer/core/command/impl/essentials/GameModeCommand.java new file mode 100644 index 0000000..6cc9b46 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/command/impl/essentials/GameModeCommand.java @@ -0,0 +1,73 @@ +package zone.themcgamer.core.command.impl.essentials; + +import org.apache.commons.lang.WordUtils; +import org.bukkit.Bukkit; +import org.bukkit.GameMode; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import zone.themcgamer.common.EnumUtils; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.data.Rank; + +public class GameModeCommand { + @Command(name = "gamemode", aliases = { "gm", "gmc", "gms", "gma", "gmsp" }, description = "Change your gamemode", + ranks = { Rank.ADMIN }) + public void onCommand(CommandProvider command) { + CommandSender sender = command.getSender(); + String[] args = command.getArgs(); + + GameMode gamemode = getGamemode(command.getLabel()); + if (args.length == 1 && gamemode == null) + gamemode = getGamemode(args[0]); + else if (args.length >= 2) gamemode = getGamemode(args[1]); + + Player target; + if (args.length < 1 && command.isPlayer()) + target = command.getPlayer(); + else target = Bukkit.getPlayer(args[0]); + + if (target == null) { + sender.sendMessage(Style.error("Gamemode", "§c" + (!command.isPlayer() ? "You must provide a player" : "Player is not online"))); + return; + } + if (gamemode == null) { + sender.sendMessage(Style.error("Gamemode", "§cInvalid gamemode given")); + return; + } + String gamemodeName = WordUtils.capitalize(gamemode.name().toLowerCase()); + target.setGameMode(gamemode); + sender.sendMessage(Style.main("Gamemode", "Updated §f" + target.getName() + "'s §7gamemode to §b" + gamemodeName)); + if (command.isPlayer() && (!command.getPlayer().equals(target))) + target.sendMessage(Style.main("Gamemode", "Your gamemode was updated to §f" + gamemodeName)); + } + + private GameMode getGamemode(String s) { + GameMode gamemode = EnumUtils.fromString(GameMode.class, s.toUpperCase()); + if (gamemode != null) + return gamemode; + switch (s.toLowerCase()) { + case "gms": + case "s": + case "0": { + return GameMode.SURVIVAL; + } + case "gmc": + case "c": + case "1": { + return GameMode.CREATIVE; + } + case "gma": + case "a": + case "2": { + return GameMode.ADVENTURE; + } + case "gmsp": + case "3": { + return GameMode.SPECTATOR; + } + } + return null; + } +} diff --git a/core/src/main/java/zone/themcgamer/core/command/impl/essentials/TeleportCommand.java b/core/src/main/java/zone/themcgamer/core/command/impl/essentials/TeleportCommand.java new file mode 100644 index 0000000..fa851bc --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/command/impl/essentials/TeleportCommand.java @@ -0,0 +1,56 @@ +package zone.themcgamer.core.command.impl.essentials; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.entity.Player; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.data.Rank; + +public class TeleportCommand { + @Command(name = "teleport", aliases = {"tp"}, ranks = Rank.HELPER, description = "Teleport to a player") + public void onCommand(CommandProvider command) { + Player player = command.getPlayer(); + String[] args = command.getArgs(); + if (args.length == 1) { + if (command.isPlayer()) { + Player target = Bukkit.getPlayer(args[0]); + if (target == null) { + command.getSender().sendMessage(Style.error("Essentials", "Player is not online!")); + return; + } + player.teleport(target); + command.getSender().sendMessage(Style.main("Essentials", "&7You have been teleported to &6" + target.getName() + "&7.")); + } + } else if (args.length == 2) { + Player target1 = Bukkit.getPlayer(args[0]); + Player target2 = Bukkit.getPlayer(args[1]); + if (target1 == null) { + command.getSender().sendMessage(Style.error("Essentials", args[0] + " is not online!")); + return; + } + if (target2 == null) { + command.getSender().sendMessage(Style.error("Essentials", args[1] + " is not online!")); + return; + } + target1.teleport(target2); + command.getSender().sendMessage(Style.main("Essentials","&7You have teleported &6" + target1.getName() + " &7to &6" + target2.getName() + "&7.")); + } else if (args.length == 3) { + double x = args[0].startsWith("~") ? player.getLocation().getX() + (args[0].length() > 1 ? Double.parseDouble(args[0].substring(1)) : 0) : Double.parseDouble(args[0]); + double y = args[1].startsWith("~") ? player.getLocation().getY() + (args[1].length() > 1 ? Double.parseDouble(args[1].substring(1)) : 0) : Double.parseDouble(args[1]); + double z = args[2].startsWith("~") ? player.getLocation().getZ() + (args[2].length() > 1 ? Double.parseDouble(args[2].substring(1)) : 0) : Double.parseDouble(args[2]); + + Location location = new Location(player.getWorld(), x, y, z, player.getLocation().getYaw(), player.getLocation().getPitch()); + player.teleport(location); + player.sendMessage(Style.main("Essentials","Teleporting to location: &6" + location.getWorld().getName() + location.getBlockX() + location.getBlockY() + location.getBlockZ())); + } else { + command.getSender().sendMessage(""); + command.getSender().sendMessage(Style.color("&6&lTeleport &eHelp")); + command.getSender().sendMessage(Style.color("&7 - &b/teleport ")); + command.getSender().sendMessage(Style.color("&7 - &b/teleport ")); + command.getSender().sendMessage(Style.color("&7 - &b/teleport ")); + command.getSender().sendMessage(""); + } + } +} diff --git a/core/src/main/java/zone/themcgamer/core/command/impl/social/IgnoreCommand.java b/core/src/main/java/zone/themcgamer/core/command/impl/social/IgnoreCommand.java new file mode 100644 index 0000000..8c7d7d5 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/command/impl/social/IgnoreCommand.java @@ -0,0 +1,4 @@ +package zone.themcgamer.core.command.impl.social; + +public class IgnoreCommand { +} diff --git a/core/src/main/java/zone/themcgamer/core/command/impl/social/MessageCommand.java b/core/src/main/java/zone/themcgamer/core/command/impl/social/MessageCommand.java new file mode 100644 index 0000000..939b2fb --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/command/impl/social/MessageCommand.java @@ -0,0 +1,73 @@ +package zone.themcgamer.core.command.impl.social; + +import com.cryptomorin.xseries.XSound; +import lombok.AllArgsConstructor; +import org.bukkit.entity.Player; +import zone.themcgamer.core.account.Account; +import zone.themcgamer.core.account.AccountManager; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.data.jedis.cache.CacheRepository; +import zone.themcgamer.data.jedis.cache.impl.PlayerStatusCache; +import zone.themcgamer.data.jedis.command.JedisCommandHandler; +import zone.themcgamer.data.jedis.command.impl.player.PlayerDirectMessageEvent; + +import java.util.Arrays; +import java.util.Optional; +import java.util.stream.Collectors; + +@AllArgsConstructor +public class MessageCommand { + private final AccountManager accountManager; + private final CacheRepository cacheRepository; + + @Command(name = "msg", aliases = { "whisper", "m", "message", "dm", "w" }, description = "Sent a private message to a player.", + playersOnly = true) + public void onCommand(CommandProvider command) { + Player player = command.getPlayer(); + String[] args = command.getArgs(); + if (args.length < 2) { + player.sendMessage(Style.main("Chat", "&7Please use &b/" + command.getLabel() + " (player) (message)")); + return; + } + String target = args[0]; + if (player.getName().equalsIgnoreCase(target)) { + player.sendMessage(Style.main("Chat", "&7You can not message yourself.")); + return; + } + String message = Arrays.stream(args).skip(1).collect(Collectors.joining(" ")); + + /* + TODO + check if player has ignored you. + check if player has messages disabled + */ + + accountManager.lookup(target, targetAccount -> { + if (targetAccount == null) { + player.sendMessage(Style.error("Chat", "That player does not exist, have they logged in before?")); + return; + } + Optional optionalAccount = AccountManager.fromCache(player.getUniqueId()); + if (optionalAccount.isEmpty()) + return; + Optional optionalPlayerStatusCache = cacheRepository.lookup(PlayerStatusCache.class, targetAccount.getUuid()); + if (optionalPlayerStatusCache.isEmpty()) { + player.sendMessage(Style.error("Chat", "That player is not online.")); + return; + } + PlayerStatusCache statusCache = optionalPlayerStatusCache.get(); + statusCache.setLastReply(player.getName()); + cacheRepository.post(statusCache); + + player.sendMessage(Style.color("&b\u2709 &7(to " + targetAccount.getDisplayName() + "&7) &8\u00BB &f" + message)); + player.playSound(player.getLocation(), XSound.ENTITY_CHICKEN_EGG.parseSound(), 0.9f, 1f); + JedisCommandHandler.getInstance().send(new PlayerDirectMessageEvent( + optionalAccount.get().getDisplayName(), + message, + targetAccount.getUuid(), + false)); + }); + } +} diff --git a/core/src/main/java/zone/themcgamer/core/command/impl/social/MessageToggleCommand.java b/core/src/main/java/zone/themcgamer/core/command/impl/social/MessageToggleCommand.java new file mode 100644 index 0000000..a551c23 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/command/impl/social/MessageToggleCommand.java @@ -0,0 +1,4 @@ +package zone.themcgamer.core.command.impl.social; + +public class MessageToggleCommand { +} diff --git a/core/src/main/java/zone/themcgamer/core/command/impl/social/ReplyCommand.java b/core/src/main/java/zone/themcgamer/core/command/impl/social/ReplyCommand.java new file mode 100644 index 0000000..d72dca4 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/command/impl/social/ReplyCommand.java @@ -0,0 +1,72 @@ +package zone.themcgamer.core.command.impl.social; + +import com.cryptomorin.xseries.XSound; +import lombok.RequiredArgsConstructor; +import org.bukkit.entity.Player; +import zone.themcgamer.core.account.Account; +import zone.themcgamer.core.account.AccountManager; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.data.jedis.cache.CacheRepository; +import zone.themcgamer.data.jedis.cache.impl.PlayerStatusCache; +import zone.themcgamer.data.jedis.command.JedisCommandHandler; +import zone.themcgamer.data.jedis.command.impl.player.PlayerDirectMessageEvent; + +import java.util.Arrays; +import java.util.Optional; +import java.util.stream.Collectors; + +@RequiredArgsConstructor +public class ReplyCommand { + private final AccountManager accountManager; + private final CacheRepository cacheRepository; + + @Command(name = "reply", aliases = { "r" }, description = "Reply to a player that sent you a private message.", playersOnly = true) + public void onCommand(CommandProvider command) { + Player player = command.getPlayer(); + String[] args = command.getArgs(); + if (args.length < 1) { + player.sendMessage(Style.main("Chat", "&7Please use &b/" + command.getLabel() + " (message)")); + return; + } + String message = Arrays.stream(args).skip(0).collect(Collectors.joining(" ")); + Optional optionalPlayerStatusCache = cacheRepository.lookup(PlayerStatusCache.class, player.getUniqueId()); + if (optionalPlayerStatusCache.isEmpty()) { + player.sendMessage(Style.error("Chat", "That player is not online.")); + return; + } + PlayerStatusCache statusCache = optionalPlayerStatusCache.get(); + if (statusCache.getLastReply().isEmpty()) { + player.sendMessage(Style.main("Chat", "&7You have nobody to reply to.")); + return; + } + accountManager.lookup(statusCache.getLastReply(), account -> { + if (account == null) { + player.sendMessage(Style.error("Chat", "That player does not exist, have they logged in before?")); + return; + } + Optional optionalTargetPlayerStatusCache = cacheRepository.lookup(PlayerStatusCache.class, account.getUuid()); + if (optionalTargetPlayerStatusCache.isEmpty()) { + player.sendMessage(Style.error("Chat", "That player is not online anymore!")); + optionalPlayerStatusCache.get().setLastReply(""); + cacheRepository.post(optionalPlayerStatusCache.get()); + return; + } + PlayerStatusCache statusTargetCache = optionalTargetPlayerStatusCache.get(); + statusTargetCache.setLastReply(player.getName()); + cacheRepository.post(statusTargetCache); + + Optional optionalAccount = AccountManager.fromCache(player.getUniqueId()); + if (optionalAccount.isEmpty()) + return; + player.sendMessage(Style.color("&b\u2709 &7(to " + account.getDisplayName() + "&7) &8\u00BB &f" + message)); + player.playSound(player.getLocation(), XSound.ENTITY_CHICKEN_EGG.parseSound(), 0.9f, 1f); + JedisCommandHandler.getInstance().send(new PlayerDirectMessageEvent( + optionalAccount.get().getDisplayName(), + message, + account.getUuid(), + true)); + }); + } +} diff --git a/core/src/main/java/zone/themcgamer/core/command/impl/staff/StaffChatCommand.java b/core/src/main/java/zone/themcgamer/core/command/impl/staff/StaffChatCommand.java new file mode 100644 index 0000000..96be949 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/command/impl/staff/StaffChatCommand.java @@ -0,0 +1,37 @@ +package zone.themcgamer.core.command.impl.staff; + +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import zone.themcgamer.core.account.Account; +import zone.themcgamer.core.account.AccountManager; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.core.plugin.MGZPlugin; +import zone.themcgamer.data.Rank; +import zone.themcgamer.data.jedis.command.JedisCommandHandler; + +import java.util.Optional; + +public class StaffChatCommand { + @Command(name = "staffchat", aliases = { "sc" }, description = "Send a message to all online staff", ranks = { Rank.HELPER }) + public void onCommand(CommandProvider command) { + CommandSender sender = command.getSender(); + String[] args = command.getArgs(); + if (args.length < 1) { + command.getPlayer().sendMessage(Style.error("StaffChat", "Please define a message!")); + return; + } + String prefix = "§4§lTerminal"; + if (sender instanceof Player) { + Optional optionalAccount = AccountManager.fromCache(command.getPlayer().getUniqueId()); + if (optionalAccount.isEmpty()) + return; + prefix = optionalAccount.get().getPrimaryRank().getPrefix(); + if (prefix == null) + prefix = ""; + } + JedisCommandHandler.getInstance().send(new zone.themcgamer.data.jedis.command.impl.StaffChatCommand( + prefix, sender.getName(), MGZPlugin.getMinecraftServer().getName(), String.join(" ", args))); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/common/BlockTag.java b/core/src/main/java/zone/themcgamer/core/common/BlockTag.java new file mode 100644 index 0000000..3b11ab8 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/common/BlockTag.java @@ -0,0 +1,122 @@ +package zone.themcgamer.core.common; + +import com.cryptomorin.xseries.XMaterial; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.bukkit.Material; +import org.bukkit.block.Block; + +import java.util.Arrays; + +/** + * @author Braydon + * @implNote This class serves the purpose of checking if a {@link XMaterial}, {@link Material}, or {@link Block} is + * of a specific type. + * @usage {@code BlockTag.TRAPDOOR.hasType(block)} check if the given {@link Block} is a type of trapdoor + */ +@AllArgsConstructor @Getter +public enum BlockTag { + TRAPDOOR(new XMaterial[] { + XMaterial.OAK_TRAPDOOR, + XMaterial.SPRUCE_TRAPDOOR, + XMaterial.BIRCH_TRAPDOOR, + XMaterial.JUNGLE_TRAPDOOR, + XMaterial.ACACIA_TRAPDOOR, + XMaterial.DARK_OAK_TRAPDOOR, + XMaterial.CRIMSON_TRAPDOOR, + XMaterial.WARPED_TRAPDOOR, + XMaterial.IRON_TRAPDOOR + }), + DOOR(new XMaterial[] { + XMaterial.OAK_DOOR, + XMaterial.SPRUCE_DOOR, + XMaterial.BIRCH_DOOR, + XMaterial.JUNGLE_DOOR, + XMaterial.ACACIA_DOOR, + XMaterial.DARK_OAK_DOOR, + XMaterial.CRIMSON_DOOR, + XMaterial.WARPED_DOOR, + XMaterial.IRON_DOOR + }), + FENCE_GATE(new XMaterial[] { + XMaterial.OAK_FENCE_GATE, + XMaterial.SPRUCE_FENCE_GATE, + XMaterial.BIRCH_FENCE_GATE, + XMaterial.JUNGLE_FENCE_GATE, + XMaterial.ACACIA_FENCE_GATE, + XMaterial.DARK_OAK_FENCE_GATE, + XMaterial.CRIMSON_FENCE_GATE, + XMaterial.WARPED_FENCE_GATE + }), + CHEST(new XMaterial[] { + XMaterial.CHEST, + XMaterial.TRAPPED_CHEST, + XMaterial.ENDER_CHEST + }), + STORAGE(new XMaterial[] { + XMaterial.CHEST, + XMaterial.TRAPPED_CHEST, + XMaterial.ENDER_CHEST, + XMaterial.CHEST_MINECART, + XMaterial.SHULKER_BOX, + XMaterial.WHITE_SHULKER_BOX, + XMaterial.ORANGE_SHULKER_BOX, + XMaterial.MAGENTA_SHULKER_BOX, + XMaterial.LIGHT_BLUE_SHULKER_BOX, + XMaterial.YELLOW_SHULKER_BOX, + XMaterial.LIME_SHULKER_BOX, + XMaterial.PINK_SHULKER_BOX, + XMaterial.GRAY_SHULKER_BOX, + XMaterial.LIGHT_GRAY_SHULKER_BOX, + XMaterial.CYAN_SHULKER_BOX, + XMaterial.PURPLE_SHULKER_BOX, + XMaterial.BLUE_SHULKER_BOX, + XMaterial.BROWN_SHULKER_BOX, + XMaterial.GREEN_SHULKER_BOX, + XMaterial.RED_SHULKER_BOX, + XMaterial.BLACK_SHULKER_BOX, + XMaterial.BARREL, + XMaterial.DISPENSER, + XMaterial.DROPPER, + XMaterial.HOPPER + // TODO: 1/29/21 add BUNDLE from Minecraft 1.17 + }), + MUSIC(new XMaterial[] { + XMaterial.JUKEBOX, + XMaterial.NOTE_BLOCK + }), + ANVIL(new XMaterial[] { + XMaterial.ANVIL, + XMaterial.CHIPPED_ANVIL, + XMaterial.DAMAGED_ANVIL + }); + + private final XMaterial[] types; + + /** + * Check if the given {@link Block} {@link Material} is apart of this {@link BlockTag} + * @param block the block to check + * @return if the block material is apart of this tag + */ + public boolean isType(Block block) { + return isType(block.getType()); + } + + /** + * Check if the given {@link Material} is apart of this {@link BlockTag} + * @param material the material to check + * @return if the material is apart of this tag + */ + public boolean isType(Material material) { + return isType(XMaterial.matchXMaterial(material)); + } + + /** + * Check if the given {@link XMaterial} is apart of this {@link BlockTag} + * @param material the material to check + * @return if the material is apart of this tag + */ + public boolean isType(XMaterial material) { + return Arrays.asList(types).contains(material); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/common/HiddenStringUtils.java b/core/src/main/java/zone/themcgamer/core/common/HiddenStringUtils.java new file mode 100644 index 0000000..8c877aa --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/common/HiddenStringUtils.java @@ -0,0 +1,127 @@ +package zone.themcgamer.core.common; + +import lombok.experimental.UtilityClass; +import org.bukkit.ChatColor; + +import java.nio.charset.StandardCharsets; + +/** + * @author Braydon + */ +@UtilityClass +public class HiddenStringUtils { + private static final String SEQUENCE_HEADER = "§r§n§r"; + private static final String SEQUENCE_FOOTER = "§r§o§r"; + + /** + * Encode the given {@link String} and return the + * encoded string + * @param s - The string to encode + * @return the encoded string + */ + public static String encode(String s) { + if (s == null) + return null; + return SEQUENCE_HEADER + stringToColors(s) + SEQUENCE_FOOTER; + } + + /** + * Replace the given from hidden {@link String} with + * the to {@link String} + * @param from - The from + * @param to - The to + * @return the replaced string + */ + public static String replaceHiddenString(String from, String to) { + if (from == null) + return null; + int start = from.indexOf(SEQUENCE_HEADER); + int end = from.indexOf(SEQUENCE_FOOTER); + if (start < 0 || end < 0) + return null; + return from.substring(0, start + SEQUENCE_HEADER.length()) + stringToColors(to) + from.substring(end); + } + + /** + * Get whether or not the provided {@link String} is + * a hidden {@link String} + * @param s - The string to check + * @return whether or not the {@link String} is a hidden {@link String} + */ + public static boolean hasHiddenString(String s) { + if (s == null) + return false; + return s.contains(SEQUENCE_HEADER) && s.contains(SEQUENCE_FOOTER); + } + + /** + * Decode the given {@link String} + * @param s - The string to decode + * @return the decoded string + */ + public static String decode(String s) { + if (s == null) + return null; + int start = s.indexOf(SEQUENCE_HEADER); + int end = s.indexOf(SEQUENCE_FOOTER); + + if (start < 0 || end < 0) + return null; + + s = s.substring(start + SEQUENCE_HEADER.length(), end); + + s = s.toLowerCase().replace("" + ChatColor.COLOR_CHAR, ""); + if (s.length() % 2 != 0) + s = s.substring(0, (s.length() / 2) * 2); + char[] chars = s.toCharArray(); + byte[] bytes = new byte[chars.length / 2]; + + for (int i = 0; i < chars.length; i+= 2) + bytes[i / 2] = hexToByte(chars[i], chars[i + 1]); + return new String(bytes, StandardCharsets.UTF_8); + } + + private String stringToColors(String s) { + if (s == null) + return null; + byte[] bytes = s.getBytes(StandardCharsets.UTF_8); + char[] chars = new char[bytes.length * 4]; + + for (int i = 0; i < bytes.length; i++) { + char[] hex = byteToHex(bytes[i]); + chars[i * 4] = ChatColor.COLOR_CHAR; + chars[i * 4 + 1] = hex[0]; + chars[i * 4 + 2] = ChatColor.COLOR_CHAR; + chars[i * 4 + 3] = hex[1]; + } + + return new String(chars); + } + + private byte hexToByte(char hex1, char hex0) { + return (byte) (((hexToUnsignedInt(hex1) << 4) | hexToUnsignedInt(hex0)) + Byte.MIN_VALUE); + } + + private int hexToUnsignedInt(char c) { + if (c >= '0' && c <= '9') { + return c - 48; + } else if (c >= 'a' && c <= 'f') { + return c - 87; + } else { + throw new IllegalArgumentException("Hex char out of range"); + } + } + + private char[] byteToHex(byte b) { + int unsignedByte = (int) b - Byte.MIN_VALUE; + return new char[] { unsignedIntToHex((unsignedByte >> 4) & 0xf), unsignedIntToHex(unsignedByte & 0xf) }; + } + + private char unsignedIntToHex(int i) { + if (i >= 0 && i <= 9) + return (char) (i + 48); + else if (i >= 10 && i <= 15) + return (char) (i + 87); + else throw new IllegalArgumentException("Hex int out of range"); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/common/ItemBuilder.java b/core/src/main/java/zone/themcgamer/core/common/ItemBuilder.java new file mode 100644 index 0000000..5f920ac --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/common/ItemBuilder.java @@ -0,0 +1,445 @@ +package zone.themcgamer.core.common; + +import com.cryptomorin.xseries.SkullUtils; +import com.cryptomorin.xseries.XEnchantment; +import com.cryptomorin.xseries.XMaterial; +import org.bukkit.Color; +import org.bukkit.DyeColor; +import org.bukkit.Material; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.inventory.meta.LeatherArmorMeta; + +import java.util.*; + +/** + * @author Braydon + */ +public class ItemBuilder { + private final ItemStack item; + + /** + * Create a new item builder from an existing {@link ItemStack} + * @param item - The {@link ItemStack} you would like to create the item builder with + */ + public ItemBuilder(ItemStack item) { + this.item = item; + } + + /** + * Create a new item builder with the provided {@link XMaterial} + * @param xMaterial - The {@link XMaterial} of the item you would like to create + */ + public ItemBuilder(XMaterial xMaterial) { + this(xMaterial, 1, xMaterial.getData()); + } + + /** + * Create a new item builder with the provided {@link Material} + * @param material - The {@link Material} of the item you would like to create + */ + public ItemBuilder(Material material) { + this(material, 1); + } + + /** + * Create a new item builder with the provided {@link XMaterial}, and amount + * @param xMaterial - The {@link XMaterial} of the item you would like to create + * @param amount - The amount of the item you would like to create + */ + public ItemBuilder(XMaterial xMaterial, int amount) { + Material material = xMaterial.parseMaterial(); + if (material == null) + material = Material.STONE; + item = new ItemStack(material, amount, xMaterial.getData()); + } + + /** + * Create a new item builder with the provided {@link Material}, and amount + * @param material - The {@link Material} of the item you would like to create + * @param amount - The amount of the item you would like to create + */ + public ItemBuilder(Material material, int amount) { + item = new ItemStack(material, amount); + } + + /** + * Create a new item builder with the provided {@link XMaterial}, amount, and dye color + * @param xMaterial - The {@link XMaterial} of the item you would like to create + * @param amount - The amount of the item you would like to create + * @param color - The dye color of the item you would like to create + */ + public ItemBuilder(XMaterial xMaterial, int amount, DyeColor color) { + this(xMaterial, amount, color.getDyeData()); + } + + /** + * Create a new item builder with the provided {@link Material}, amount, and dye color + * @param material - The {@link Material} of the item you would like to create + * @param amount - The amount of the item you would like to create + * @param color - The dye color of the item you would like to create + */ + public ItemBuilder(Material material, int amount, DyeColor color) { + this(material, amount, color.getDyeData()); + } + + /** + * Create a new item builder with the provided {@link XMaterial}, amount, and data + * @param xMaterial - The {@link XMaterial} of the item you would like to create + * @param amount - The amount of the item you would like to create + * @param data - The data of the item you would like to create + */ + public ItemBuilder(XMaterial xMaterial, int amount, byte data) { + Material material = xMaterial.parseMaterial(); + if (material == null) + material = Material.STONE; + item = new ItemStack(material, amount, data); + } + + /** + * Create a new item builder with the provided {@link Material}, amount, and data + * @param material - The {@link Material} of the item you would like to create + * @param amount - The amount of the item you would like to create + * @param data - The data of the item you would like to create + */ + public ItemBuilder(Material material, int amount, byte data) { + item = new ItemStack(material, amount, data); + } + + /** + * Sets the type of your item to the provided {@link XMaterial} + * @param xMaterial - The {@link XMaterial} you would like to set your item to + * @return the item builder + */ + public ItemBuilder setType(XMaterial xMaterial) { + Material material = xMaterial.parseMaterial(); + if (material == null) + material = Material.STONE; + return setType(material); + } + + /** + * Sets the type of your item to the provided {@link Material} + * @param material - The {@link Material} you would like to set your item to + * @return the item builder + */ + public ItemBuilder setType(Material material) { + item.setType(material); + return this; + } + + /** + * Sets the data of your item to the provided data + * @param data - The data you would like to set your item to + * @return the item builder + */ + public ItemBuilder setData(byte data) { + item.setDurability(data); + return this; + } + + /** + * Sets the display name of your item to the provided string + * @param name - The name you would like to set your item to + * @return the item builder + */ + public ItemBuilder setName(String name) { + ItemMeta meta = item.getItemMeta(); + if (name != null) + meta.setDisplayName(Style.color(name)); + else meta.setDisplayName("§a"); + item.setItemMeta(meta); + return this; + } + + /** + * Sets the lore of your item to the provided string array + * @param array - The array of strings you would like to set your lore to + * @return the item builder + */ + public ItemBuilder setLore(String... array) { + return setLore(Arrays.asList(array)); + } + + /** + * Sets the lore of your item to the provided string list + * @param list - The list of strings you would like to set your lore to + * @return the item builder + */ + public ItemBuilder setLore(List list) { + ItemMeta meta = item.getItemMeta(); + meta.setLore(Style.colorLines(list)); + item.setItemMeta(meta); + return this; + } + + /** + * Add a string to the item lore at the provided index + * @param index - The index you would like to add the lore line to + * @param s - The text you would like to add at the provided index + * @return the item builder + */ + public ItemBuilder addLoreLine(int index, String s) { + ItemMeta meta = item.getItemMeta(); + List lore = new ArrayList<>(meta.getLore()); + lore.set(index, Style.color(s)); + meta.setLore(lore); + item.setItemMeta(meta); + return this; + } + + /** + * Add a string to the item lore + * @param s - The text you would like to add to your item lore + * @return the item builder + */ + public ItemBuilder addLoreLine(String s) { + ItemMeta meta = item.getItemMeta(); + List lore = new ArrayList<>(); + if (meta.hasLore()) + lore = new ArrayList<>(meta.getLore()); + lore.add(Style.color(s)); + meta.setLore(lore); + item.setItemMeta(meta); + return this; + } + + /** + * Remove a string from the lore at the provided index + * @param index - The index you would like to remove the lore line from + * @return the item builder + */ + public ItemBuilder removeLoreLine(int index) { + ItemMeta meta = item.getItemMeta(); + List lore = new ArrayList<>(meta.getLore()); + if (index < 0 || index > lore.size()) + return this; + lore.remove(index); + meta.setLore(lore); + item.setItemMeta(meta); + return this; + } + + /** + * Remove a string from the lore that matches the provided string + * @param s - The string you would like to remove from the lore + * @return the item builder + */ + public ItemBuilder removeLoreLine(String s) { + ItemMeta meta = item.getItemMeta(); + List lore = new ArrayList<>(meta.getLore()); + if (!lore.contains(s)) + return this; + lore.remove(s); + meta.setLore(lore); + item.setItemMeta(meta); + return this; + } + + /** + * Clears the item lore + * @return the item builder + */ + public ItemBuilder clearLore() { + ItemMeta meta = item.getItemMeta(); + List lore = new ArrayList<>(meta.getLore()); + lore.clear(); + meta.setLore(lore); + item.setItemMeta(meta); + return this; + } + + /** + * Set the amount of the itemstack + * @param amount - The amount you would like your item to be + * @return the item builder + */ + public ItemBuilder setAmount(int amount) { + item.setAmount(amount); + return this; + } + + /** + * Set the durability of the itemstack + * @param durability - The durability you would like your item to have + * @return the item builder + */ + public ItemBuilder setDurability(short durability) { + item.setDurability(durability); + return this; + } + + /** + * Add a glow effect to your item + * @return the item builder + */ + public ItemBuilder addGlow() { + return setGlow(true); + } + + /** + * Add a glow effect to your item + * @return the item builder + */ + public ItemBuilder setGlow(boolean glow) { + Enchantment enchantment = XEnchantment.ARROW_DAMAGE.parseEnchantment(); + if (enchantment == null) + return this; + if (glow) { + item.addUnsafeEnchantment(enchantment, 1); + ItemMeta meta = item.getItemMeta(); + meta.addItemFlags(ItemFlag.HIDE_ENCHANTS); + item.setItemMeta(meta); + } else item.removeEnchantment(enchantment); + return this; + } + + /** + * Adds enchantments to your item from a map ({@link XEnchantment}, {@link Integer}) + * @param enchantments - A map of enchantments with the key as the enchant, and the value as the level + * @return the item builder + */ + public ItemBuilder addXEnchantments(Map enchantments) { + Map bukkitEnchantments = new HashMap<>(); + for (Map.Entry entry : enchantments.entrySet()) { + Enchantment bukkitEnchantment = entry.getKey().parseEnchantment(); + if (bukkitEnchantment == null) + continue; + bukkitEnchantments.put(bukkitEnchantment, entry.getValue()); + } + return addEnchantments(bukkitEnchantments); + } + + /** + * Adds enchantments to your item from a map ({@link Enchantment}, {@link Integer}) + * @param enchantments - A map of enchantments with the key as the enchant, and the value as the level + * @return the item builder + */ + public ItemBuilder addEnchantments(Map enchantments) { + item.addEnchantments(enchantments); + return this; + } + + /** + * Add an enchantment to your item + * @param xEnchantment - The {@link XEnchantment} you would like to add + * @param level - The level of the enchantment to add + * @return the item builder + */ + public ItemBuilder addEnchant(XEnchantment xEnchantment, int level) { + Enchantment enchantment = xEnchantment.parseEnchantment(); + if (enchantment == null) + return this; + return addEnchant(enchantment, level); + } + + /** + * Add an enchantment to your item + * @param enchantment - The {@link Enchantment} you would like to add + * @param level - The level of the enchantment to add + * @return the item builder + */ + public ItemBuilder addEnchant(Enchantment enchantment, int level) { + ItemMeta meta = item.getItemMeta(); + meta.addEnchant(enchantment, level, true); + item.setItemMeta(meta); + return this; + } + + /** + * Add an unsafe enchantment to your item + * @param xEnchantment - The {@link XEnchantment} you would like to add + * @param level - The level of the enchant + * @return the item builder + */ + public ItemBuilder addUnsafeEnchantment(XEnchantment xEnchantment, int level) { + Enchantment enchantment = xEnchantment.parseEnchantment(); + if (enchantment == null) + return this; + return addUnsafeEnchantment(enchantment, level); + } + + /** + * Add an unsafe enchantment to your item + * @param enchantment - The {@link Enchantment} you would like to add + * @param level - The level of the enchant + * @return the item builder + */ + public ItemBuilder addUnsafeEnchantment(Enchantment enchantment, int level) { + item.addUnsafeEnchantment(enchantment, level); + return this; + } + + /** + * Remove an enchantment from your item + * @param xEnchantment - The {@link XEnchantment} you would like to remove + * @return the item builder + */ + public ItemBuilder removeEnchantment(XEnchantment xEnchantment) { + Enchantment enchantment = xEnchantment.parseEnchantment(); + if (enchantment == null) + return this; + return removeEnchantment(enchantment); + } + + /** + * Remove an enchantment from your item + * @param enchantment - The {@link Enchantment} you would like to remove + * @return the item builder + */ + public ItemBuilder removeEnchantment(Enchantment enchantment) { + item.removeEnchantment(enchantment); + return this; + } + + + /** + * Clear all enchantments from your item + * @return the item builder + */ + public ItemBuilder clearEnchantments() { + for (Enchantment enchantment : item.getItemMeta().getEnchants().keySet()) { + item.removeEnchantment(enchantment); + } + return this; + } + + /** + * Set the skull texture of your item with the provided player's skin + * @param identifier - The identifier for the skull + * @return the item builder + */ + public ItemBuilder setSkullOwner(String identifier) { + Material targetMaterial = XMaterial.PLAYER_HEAD.parseMaterial(); + if (targetMaterial == null) + return this; + if (item.getType() != targetMaterial) + throw new IllegalStateException("You cannot set the skull owner with type '" + item.getType().name() + "', it must be of type " + targetMaterial.name()); + item.setItemMeta(SkullUtils.applySkin(item.getItemMeta(), identifier)); + return this; + } + + /** + * Set the color of your item + * @param color - The color you would like to set your leather armor to + * @return the item builder + */ + public ItemBuilder setLeatherArmorColor(Color color) { + if (!item.getType().name().contains("LEATHER") || item.getType() == Material.LEATHER) + throw new IllegalStateException("You cannot set the leather armor color with type '" + item.getType().name() + "', it must be a piece of leather armor"); + LeatherArmorMeta meta = (LeatherArmorMeta) item.getItemMeta(); + meta.setColor(color); + item.setItemMeta(meta); + return this; + } + + /** + * Create the item we just created with the item builder + * @return the constructed {@link ItemStack} + */ + public ItemStack toItemStack() { + return item; + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/common/LocationUtils.java b/core/src/main/java/zone/themcgamer/core/common/LocationUtils.java new file mode 100644 index 0000000..b03c69d --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/common/LocationUtils.java @@ -0,0 +1,44 @@ +package zone.themcgamer.core.common; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; + +/** + * @author Braydon + */ +public class LocationUtils { + /** + * Serialize the given {@link Location} + * @param location the location to serialize + * @return the serialized location + */ + public static String toString(Location location) { + if (location == null) + return "null"; + return location.getWorld().getName() + "|" + + location.getX() + "|" + + location.getY() + "|" + + location.getZ() + "|" + + location.getYaw() + "|" + + location.getPitch(); + } + + /** + * Deserialize the given {@link String} + * @param s the string to deserialize + * @return the deserialized {@link Location} + */ + public static Location fromString(String s) { + if (s == null || (s.equals("null") || s.trim().isEmpty())) + return null; + String[] data = s.split("\\|"); + World world = Bukkit.getWorld(data[0]); + double x = Double.parseDouble(data[1]); + double y = Double.parseDouble(data[2]); + double z = Double.parseDouble(data[3]); + float yaw = Float.parseFloat(data[4]); + float pitch = Float.parseFloat(data[5]); + return new Location(world, x, y, z, yaw, pitch); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/common/MathUtils.java b/core/src/main/java/zone/themcgamer/core/common/MathUtils.java new file mode 100644 index 0000000..647d4c1 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/common/MathUtils.java @@ -0,0 +1,47 @@ +package zone.themcgamer.core.common; + +import org.bukkit.Location; +import org.bukkit.util.Vector; + +import java.util.Collection; + +/** + * @author Braydon + */ +public class MathUtils { + public static float getFacingYaw(Location location, Collection targetLocations) { + if (targetLocations.isEmpty()) + return 0f; + return getYaw(getTrajectory(location, findClosest(location, targetLocations))); + } + + public static Location findClosest(Location center, Collection locations) { + Location bestLocation = null; + double lastDistance = 0; + for (Location location : locations) { + double distance = center.toVector().subtract(location.toVector()).length(); + if (bestLocation == null || distance < lastDistance) { + bestLocation = location; + lastDistance = distance; + } + } + return bestLocation; + } + + public static Vector getTrajectory(Location from, Location to) { + return getTrajectory(from.toVector(), to.toVector()); + } + + public static Vector getTrajectory(Vector from, Vector to) { + return to.subtract(from).normalize(); + } + + public static float getYaw(Vector vector) { + double x = vector.getX(); + double z = vector.getZ(); + double yaw = Math.toDegrees(Math.atan((-x) / z)); + if (z < 0) + yaw+= 180; + return (float) yaw; + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/common/MiscUtils.java b/core/src/main/java/zone/themcgamer/core/common/MiscUtils.java new file mode 100644 index 0000000..075d2d8 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/common/MiscUtils.java @@ -0,0 +1,19 @@ +package zone.themcgamer.core.common; + +import java.text.DecimalFormat; + +/** + * @author Braydon + */ +public class MiscUtils { + /** + * Format the given tps value + * @param tps the tps + * @return the formatted tps + */ + public static String formatTps(double tps) { + tps = Double.parseDouble(new DecimalFormat("#.##").format(tps)); + return ((tps > 18.0) ? "§a" : (tps > 16.0) ? "§e" : "§c") + + ((tps > 20.0) ? "*" : "" ) + Math.min(Math.round(tps * 100.0) / 100.0, 20.0); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/common/MojangUtils.java b/core/src/main/java/zone/themcgamer/core/common/MojangUtils.java new file mode 100644 index 0000000..1bc01bb --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/common/MojangUtils.java @@ -0,0 +1,355 @@ +package zone.themcgamer.core.common; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.UtilityClass; +import okhttp3.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; +import zone.themcgamer.common.EnumUtils; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +/** + * @author Braydon + */ +@UtilityClass +public class MojangUtils { + private static final OkHttpClient client = new OkHttpClient(); + private static final Gson gson = new Gson(); + + private static final Cache UUID_CACHE = CacheBuilder.newBuilder() + .expireAfterWrite(16, TimeUnit.HOURS) + .build(); + private static final Cache> NAME_CHANGE_CACHE = CacheBuilder.newBuilder() + .expireAfterWrite(1, TimeUnit.HOURS) + .build(); + private static final Cache SKIN_CACHE = CacheBuilder.newBuilder() + .expireAfterWrite(10, TimeUnit.MINUTES) + .build(); + + /** + * Get the Mojang service statuses + * @return service statuses + */ + public static Map getServiceStatus() { + Map serviceStatusMap = new HashMap<>(); + try { + Request request = new Request.Builder() + .url("https://status.mojang.com/check") + .build(); + try (Response response = client.newCall(request).execute()) { + if (response.code() == 200) { + ResponseBody body = response.body(); + if (body != null) { + List> data = gson.fromJson(body.string(), new TypeToken>>() {}.getType()); + for (Map map : data) { + for (Map.Entry entry : map.entrySet()) { + MojangService service = MojangService.lookupFromPath(entry.getKey()); + if (service == null) { + System.err.println("Failed to find Mojang service: " + entry.getKey()); + continue; + } + ServiceStatus status = EnumUtils.fromString(ServiceStatus.class, entry.getValue().toUpperCase()); + if (status == null) { + System.err.println("Failed to find status for service '" + service.name() + "': " + entry.getValue()); + continue; + } + serviceStatusMap.put(service, status); + } + } + } + } + } + } catch (IOException ex) { + ex.printStackTrace(); + } + return serviceStatusMap; + } + + /** + * Get the {@link UUID} of the given player + * @param playerName - The name of the player to get the uuid for + * @return the uuid + */ + @Nullable + public static UUID getUUIDSync(String playerName) { + UUID uuid = UUID_CACHE.getIfPresent(playerName); + if (uuid == null) { + try { + Request request = new Request.Builder() + .url("https://api.mojang.com/users/profiles/minecraft/" + playerName) + .build(); + try (Response response = client.newCall(request).execute()) { + if (response.code() == 200) { + ResponseBody body = response.body(); + if (body != null) { + Map data = gson.fromJson(body.string(), new TypeToken>() {}.getType()); + if (data.containsKey("id")) { + uuid = parseUUID(data.get("id")); + UUID_CACHE.put(playerName, uuid); + } + } + } + } + } catch (IOException ex) { + ex.printStackTrace(); + } + } + return uuid; + } + + /** + * Get the {@link UUID} of the given player asynchronously + * @param playerName - The name of the player to get the uuid for + * @param callback - The consumer that that will contain the uuid response, null if none + */ + public static void getUUIDAsync(String playerName, Consumer callback) { + UUID cachedUUID = UUID_CACHE.getIfPresent(playerName); + if (cachedUUID != null) { + callback.accept(cachedUUID); + return; + } + Request request = new Request.Builder() + .url("https://api.mojang.com/users/profiles/minecraft/" + playerName) + .build(); + client.newCall(request).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + if (response.code() != 200) { + callback.accept(null); + return; + } + ResponseBody body = response.body(); + if (body == null) { + callback.accept(null); + return; + } + Map data = gson.fromJson(body.string(), new TypeToken>() {}.getType()); + if (data.containsKey("id")) { + UUID uuid = parseUUID(data.get("id")); + UUID_CACHE.put(playerName, uuid); + callback.accept(uuid); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException ex) { + callback.accept(null); + } + }); + } + + /** + * Get a list of changes names for the given uuid + * The response is: name, timestamp (-1 if first name) + * @param uuid - The {@link UUID} to get the changed names for + * @return the changed names + */ + public static Map getNameChangesSync(UUID uuid) { + Map nameChanges = NAME_CHANGE_CACHE.getIfPresent(uuid); + if (nameChanges == null) { + nameChanges = new HashMap<>(); + try { + Request request = new Request.Builder() + .url("https://api.mojang.com/user/profiles/" + uuid.toString().replaceAll("-", "") + "/names") + .build(); + try (Response response = client.newCall(request).execute()) { + if (response.code() == 200) { + ResponseBody body = response.body(); + if (body != null) { + List> data = gson.fromJson(body.string(), new TypeToken>>() {}.getType()); + for (Map map : data) { + nameChanges.put(map.get("name"), map.containsKey("changedToAt") ? Long.parseLong(map.get("changedToAt")) : -1L); + } + NAME_CHANGE_CACHE.put(uuid, nameChanges); + } + } + } + } catch (IOException ex) { + ex.printStackTrace(); + } + } + return nameChanges; + } + + /** + * Get a list of changes names for the given uuid asynchronously + * The response is: name, timestamp (-1 if first name) + * @param uuid - The {@link UUID} to get the changed names for + * @param callback - The consumer that that will contain the list of changed names + */ + public static void getNameChangesAsync(UUID uuid, Consumer> callback) { + Map nameChangeCache = NAME_CHANGE_CACHE.getIfPresent(uuid); + if (nameChangeCache != null) { + callback.accept(nameChangeCache); + return; + } + Request request = new Request.Builder() + .url("https://api.mojang.com/user/profiles/" + uuid.toString().replaceAll("-", "") + "/names") + .build(); + Map nameChanges = new HashMap<>(); + client.newCall(request).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + if (response.code() != 200) { + callback.accept(nameChanges); + return; + } + ResponseBody body = response.body(); + if (body == null) { + callback.accept(nameChanges); + return; + } + List> data = gson.fromJson(body.string(), new TypeToken>>() {}.getType()); + for (Map map : data) { + nameChanges.put(map.get("name"), map.containsKey("changedToAt") ? Long.parseLong(map.get("changedToAt")) : -1L); + } + NAME_CHANGE_CACHE.put(uuid, nameChanges); + callback.accept(nameChanges); + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException ex) { + callback.accept(nameChanges); + } + }); + } + + /** + * Get the skin textures for the given {@link UUID} + * @param uuid - The uuid to get the textures for + * @return the skin data + */ + public static SkinData getSkinTexturesSync(UUID uuid) { + SkinData skinData = SKIN_CACHE.getIfPresent(uuid); + if (skinData == null) { + skinData = new SkinData("", null); + try { + Request request = new Request.Builder() + .url("https://sessionserver.mojang.com/session/minecraft/profile/" + uuid.toString().replaceAll("-", "") + "?unsigned=false") + .build(); + try (Response response = client.newCall(request).execute()) { + if (response.code() == 200) { + ResponseBody body = response.body(); + if (body != null) { + JSONObject properties = (JSONObject) ((JSONArray) ((JSONObject) new JSONParser().parse(body.string())).get("properties")).get(0); + skinData.setValue((String) properties.get("value")); + skinData.setSignature((String) properties.get("signature")); + SKIN_CACHE.put(uuid, skinData); + } + } + } + } catch (IOException | ParseException ex) { + ex.printStackTrace(); + } + } + return skinData; + } + + /** + * Get the skin textures for the given {@link UUID} asynchronously + * @param uuid - The uuid to get the textures for + * @param callback - The consumer that that will contain the skin data + */ + public static void getSkinTexturesAsync(UUID uuid, Consumer callback) { + SkinData texturesCache = SKIN_CACHE.getIfPresent(uuid); + if (texturesCache != null) { + callback.accept(texturesCache); + return; + } + SkinData skinData = new SkinData("", null); + Request request = new Request.Builder() + .url("https://sessionserver.mojang.com/session/minecraft/profile/" + uuid.toString().replaceAll("-", "") + "?unsigned=false") + .build(); + client.newCall(request).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + if (response.code() != 200) { + callback.accept(skinData); + return; + } + ResponseBody body = response.body(); + if (body != null) { + try { + JSONObject properties = (JSONObject) ((JSONArray) ((JSONObject) new JSONParser().parse(body.string())).get("properties")).get(0); + skinData.setValue((String) properties.get("value")); + skinData.setSignature((String) properties.get("signature")); + SKIN_CACHE.put(uuid, skinData); + callback.accept(skinData); + } catch (ParseException ex) { + ex.printStackTrace(); + } + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException ex) { + callback.accept(skinData); + } + }); + } + + /** + * Parse a uuid without dashes to a proper {@link UUID} + * @param s - The un-parsed uuid + * @return the uuid + */ + private UUID parseUUID(String s) { + return UUID.fromString(s.substring(0, 8) + "-" + s.substring(8, 12) + "-" + s.substring(12, 16) + "-" + s.substring(16, 20) + "-" + s.substring(20, 32)); + } + + @AllArgsConstructor @Getter + public enum MojangService { + MINECRAFT_WEBSITE("minecraft.net"), + SESSION("session.minecraft.net"), + ACCOUNT("account.mojang.com"), + AUTH_SERVER("authserver.mojang.com"), + SESSION_SERVER("sessionserver.mojang.com"), + API("api.mojang.com"), + TEXTURES("textures.minecraft.net"), + MOJANG_WEBSITE("mojang.com"); + + private final String path; + + /** + * Get the Mojang service with the given path + * @param path - The path + * @return the Mojang service + */ + @Nullable + public static MojangService lookupFromPath(String path) { + for (MojangService service : values()) { + if (service.getPath().equals(path)) { + return service; + } + } + return null; + } + } + + public enum ServiceStatus { + GREEN, YELLOW, RED + } + + @AllArgsConstructor @Setter @Getter + public static class SkinData { + private String value; + @Nullable private String signature; + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/common/PageBuilder.java b/core/src/main/java/zone/themcgamer/core/common/PageBuilder.java new file mode 100644 index 0000000..727b25d --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/common/PageBuilder.java @@ -0,0 +1,54 @@ +package zone.themcgamer.core.common; + +import lombok.RequiredArgsConstructor; +import org.bukkit.command.CommandSender; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.BiConsumer; + +/** + * This utility makes it easy to create pages + * @author Braydon + */ +@RequiredArgsConstructor +public class PageBuilder { + private final Collection collection; + private final BiConsumer contentConsumer; + private int resultsPerPage = 7; + + /** + * Set the results per page + * @param resultsPerPage the results per page + */ + public PageBuilder resultsPerPage(int resultsPerPage) { + this.resultsPerPage = resultsPerPage; + return this; + } + + /** + * Get the maximum number of pages + * @return the maximum number of pages + */ + public int getMaxPages() { + if (collection.size() <= resultsPerPage) + return 1; + return collection.size() / resultsPerPage + 1; + } + + public void send(CommandSender sender, int page) { + if (collection.isEmpty()) { + sender.sendMessage("§cNo entries found."); + return; + } + int maxPages = getMaxPages(); + if (page <= 0 || page > maxPages) { + sender.sendMessage("§cPage out of bounds. There " + (maxPages == 1 ? "is" : "are") + " only §l" + maxPages + " §cpage" + (maxPages == 1 ? "" : "s") + "."); + return; + } + List list = new ArrayList<>(collection); + for (int i = resultsPerPage * (page - 1); i < resultsPerPage * page && i < collection.size(); i++) + contentConsumer.accept(i + 1, list.get(i)); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/common/PlayerUtils.java b/core/src/main/java/zone/themcgamer/core/common/PlayerUtils.java new file mode 100644 index 0000000..063a8b7 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/common/PlayerUtils.java @@ -0,0 +1,45 @@ +package zone.themcgamer.core.common; + +import org.bukkit.GameMode; +import org.bukkit.entity.Player; +import org.bukkit.potion.PotionEffect; + +/** + * @author Braydon + */ +public class PlayerUtils { + /** + * Reset the given {@link Player} to it's original state + * @param player the player to reset + * @param effects whether or not to clear the player's effects + * @param inventory whether or not to clear the player's inventory + * @param gameMode the {@link GameMode} to put the player in + */ + public static void reset(Player player, boolean effects, boolean inventory, GameMode gameMode) { + if (effects) { + for (PotionEffect potionEffect : player.getActivePotionEffects()) + player.removePotionEffect(potionEffect.getType()); + player.getActivePotionEffects().clear(); + } + if (gameMode != null) + player.setGameMode(gameMode); + player.setAllowFlight(false); + player.setSprinting(false); + player.setFoodLevel(20); + player.setSaturation(3.0f); + player.setExhaustion(0.0f); + player.setMaxHealth(20.0); + player.setHealth(player.getMaxHealth()); + player.setFireTicks(0); + player.setFallDistance(0.0f); + player.setLevel(0); + player.setExp(0.0f); + player.setWalkSpeed(0.2f); + player.setFlySpeed(0.1f); + if (inventory) { + player.getInventory().clear(); + player.getInventory().setArmorContents(null); + player.updateInventory(); + } + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/common/ServerUtils.java b/core/src/main/java/zone/themcgamer/core/common/ServerUtils.java new file mode 100644 index 0000000..236d9c1 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/common/ServerUtils.java @@ -0,0 +1,52 @@ +package zone.themcgamer.core.common; + +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.entity.Player; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * @author Braydon + */ +public class ServerUtils { + private static String VERSION = Bukkit.getServer().getClass().getPackage().getName(); + private static Object SERVER_OBJECT; + private static Field RECENT_TPS_FIELD; + + static { + VERSION = VERSION.substring(VERSION.lastIndexOf('.') + 1); + + try { + Class clazz = Class.forName("net.minecraft.server." + VERSION + ".MinecraftServer"); + SERVER_OBJECT = clazz.getMethod("getServer").invoke(null); + RECENT_TPS_FIELD = SERVER_OBJECT.getClass().getField("recentTps"); + } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException | NoSuchFieldException ex) { + ex.printStackTrace(); + } + } + + /** + * Get the current tps of the server + * @return the tps + */ + public static double getTps() { + try { + return ((double[]) RECENT_TPS_FIELD.get(SERVER_OBJECT))[0]; + } catch (IllegalAccessException ex) { + ex.printStackTrace(); + } + return 20D; + } + + public static List getLoadedPlayers() { + List list = new ArrayList<>(); + for (World world : Bukkit.getWorlds()) + list.addAll(world.getPlayers()); + return Collections.unmodifiableList(list); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/common/ServerVersion.java b/core/src/main/java/zone/themcgamer/core/common/ServerVersion.java new file mode 100644 index 0000000..37219ed --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/common/ServerVersion.java @@ -0,0 +1,48 @@ +package zone.themcgamer.core.common; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.bukkit.Bukkit; + +import java.util.Arrays; + +/** + * @author Braydon + */ +@AllArgsConstructor @Getter +public enum ServerVersion { + v1_8_R1(new String[] { "1.8", "1.8.1", "1.8.2" }, false), + v1_8_R2(new String[] { "1.8.3" }, false), + v1_8_R3(new String[] { "1.8.4", "1.8.5", "1.8.6", "1.8.7", "1.8.8", "1.8.9" }, false), + v1_9_R1(new String[] { "1.9", "1.9.1", "1.9.2", "1.9.3" }, false), + v1_9_R2(new String[] { "1.9.4" }, false), + v1_10_R1(new String[] { "1.10", "1.10.1", "1.10.2" }, false), + v1_11_R1(new String[] { "1.11", "1.11.1", "1.11.2" }, false), + v1_12_R1(new String[] { "1.12", "1.12.1", "1.12.2" }, false), + v1_13_R1(new String[] { "1.13" }, true), + v1_13_R2(new String[] { "1.13.1", "1.13.2" }, true), + v1_14_R1(new String[] { "1.14", "1.14.1", "1.14.2", "1.14.3" }, true), + v1_14_R2(new String[] { "1.14.4" }, true), + v1_15_R1(new String[] { "1.15", "1.15.1", "1.15.2" }, true), + v1_16_R1(new String[] { "1.16.1" }, true), + v1_16_R2(new String[] { "1.16.2", "1.16.3" }, true), + v1_16_R3(new String[] { "1.16.4", "1.16.5" }, true); + + public static String NMS_VERSION; + @Getter private static ServerVersion version; + + static { + NMS_VERSION = Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3];; + String bukkitVersion = Bukkit.getVersion().replace("(MC: ", "").replace(")", "").split(" ")[1].trim(); + ServerVersion.version = Arrays.stream(values()) + .filter(serverVersion -> Arrays.asList(serverVersion.getNames()).contains(bukkitVersion)) + .findFirst().orElse(null); + } + + private final String[] names; + private final boolean nativeVersion; + + public boolean isLegacy() { + return !nativeVersion; + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/common/SkullTexture.java b/core/src/main/java/zone/themcgamer/core/common/SkullTexture.java new file mode 100644 index 0000000..747f863 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/common/SkullTexture.java @@ -0,0 +1,34 @@ +package zone.themcgamer.core.common; + +/** + * @author Braydon + * @implNote A list of skull textures + */ +public class SkullTexture { + public static final String IRON_BLOCK = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZmZmZjNhODdlYTJhNjNiNWI3MWM0Mjk3ZGRhN2JmZjJiY2RiNjhiNjJkMWQ1ZTJiZDZlNDNiNTM4MGY5ZWIyOCJ9fX0="; + public static final String GOLD_BLOCK = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZGExNGZmOTEyNTY2ZDQ3MTk4MTI3ODhlNjMzYmE0MjNkMWRiMTQ0OWFkZmJiNzA2MWZhZmU3NGJhODgwOTQ2OSJ9fX0="; + public static final String DIAMOND_BLOCK = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMWUwNTc5OTZjYmE0NzMyOGRmNzJmYmEzZWEyYjlhYTM1YzhhODIyN2YxY2VjODljMTg4NGRjYWRhYTgyNGQ4NSJ9fX0="; + public static final String EMERALD_BLOCK = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYjcyYzA1ZGQ3NjI4OGY0MzI4YTI0MzkxYmY0NzI1ZmQyMjYwNTkyZGIzY2Y5YjJiYzIwMzJkZDA1OTZjZjQ0MCJ9fX0="; + public static final String COAL_BLOCK = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZDdmNTc2NmQyOTI4ZGMwZGYxYjM0MDRjM2JkMDczYzk0NzZkMjZjODA1NzNiMDMzMmU3Y2NlNzNkZjE1NDgyYSJ9fX0="; + public static final String GREEN_BACKPACK = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMjhhMTI3ZjFjZmQ3OTk4NmU3YmQ5NWQ5MmRlNGY0ZjY4MDQwZTRmODk5ZjgxYjFmOGYzY2ExNWI2NGY1MGYzIn19fQ=="; + public static final String DISCORD = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMzYyYTFiNDczYmEzYzI1ZTZlYjE0NThkZmM5NTM5MjFmNWQ5MTQ3OGFmOWQ2ZGRhMDdjMDMyZjkyOGMifX19"; + + // Team Colors (https://minecraft-heads.com/custom-heads/search?searchword=Leather+Helmet) + public static final String TEAM_UNDYED = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNjBiZjFkNjQzOWM5OGI0MDY2MTdhOWIxZjdjODM0NGVjZGJiODJhYjkyMTNkMzEzZWRiMjgyMTY2OTQ2MmJkZiJ9fX0="; + public static final String TEAM_PINK = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOTQ5ZDI3ZDY4YWM3M2NkZjQzNzVjOGY3MjZiZTJmN2Y3MWE1NzM4ZDU1YjhmODMyOTBmNDExZjU0Nzk2ZjBkZCJ9fX0="; + public static final String TEAM_MAGENTA = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOTZjNjViM2Q5NmU0ODNhNjBiNmViOWJjZWFlNTlkYWYwZTM1NzNjYTA1ZjJhNDdjNzg5ODEzMzE4MDg1ZjMyMSJ9fX0="; + public static final String TEAM_PURPLE = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOTA4MDNmY2M4YTk0MGNkNDllZDQ5NmE0YTVlNTVjYTA5ZjRmNDI4ZTVmMWE0ZWQ3YWM4ZjIzMDE0NWEwZTUxZCJ9fX0="; + public static final String TEAM_BLUE = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZmQ4MTJkYjcxZDRlN2JhYzAyMWQ1MzQyOTQ0MGM3Y2I3NTM2MzI3NjYxYjJlOWEzYzZlZGQwOWM4ZTYxM2RlIn19fQ=="; + public static final String TEAM_LIGHT_BLUE = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOTEzMTY1YjViOTg3ZmYwODk3ZmQ4OGY0N2ZiYmVkYWJhOTIyNzk2NmRlYjM2YzhmNGY3NWFmYTE5NGFhY2I5NiJ9fX0="; + public static final String TEAM_CYAN = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNGRkMzljNDUwMzRiNzEzZDNmNGIyMDY5NWQzNDllZTlmY2Y4YTViNDgwMWNlZTVjOTlhNTlmZThmMjYwZmIzZCJ9fX0="; + public static final String TEAM_GREEN = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYWQ2OTQ3MTRlMzRiYzQwODY5ZjQ0ODdmODFhMDQzNGVjYjRmZWU1M2VhY2JhNzc5NjcyYTk0OTQ1NTNjZjVkYiJ9fX0="; + public static final String TEAM_LIME = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMjE3YjZhMDY3N2VmMmY1YWM0MzAzMTgyYTRkNmE2ZGE4NjE4Y2U3YzAyNmE0YWJlY2FmY2Q3NWRhNTI2MmM5OCJ9fX0="; + public static final String TEAM_YELLOW = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMWNjNGFmY2QzOTRiMjEwNzc2YWEyM2E2YzdkMDZmZTJkYmMwZmZmMDU3ZWFlZGZkMWJiNmVkZDA4MDAxMzVjNCJ9fX0="; + public static final String TEAM_ORANGE = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNmNkYzhjZDdmMDcyNDRmMTI0NDI2MmVlMTFhYTBlN2Q5MTllODcwZTFmZDVhM2UyZDNiM2RlNTgwMmJmOTUxYSJ9fX0="; + public static final String TEAM_RED = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZTcyYjRhNWUzNTk5MTA2MTIxZmFmOTNkNGIyY2E3ZGE5OTg0MGMwYTgwYWU0YjZkNGEzYWE1ZjVhYzQyY2IwZiJ9fX0="; + public static final String TEAM_BROWN = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYTgwNGI4MjU2N2FjYmFlN2MwZmFmZTY1MzhmZDBhZDZhODI2YmQyZjFmZmE5ZjJmZmEwY2I3OGMwYzJkMjhkMiJ9fX0="; + public static final String TEAM_WHITE = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNTRjNjY5YWQzYzdmM2ZjODU2NjU1MjFkMGUwMDBmZGQ0MjUwYWIzZThhYWMxOGFmZGNmOWQzYjM1ZjVkMWZkZiJ9fX0="; + public static final String TEAM_LIGHT_GRAY = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZmNkYmJlYzJlN2M2Zjc5OTY2OTkyZThkM2I2OTAwZjk4ZGFhNGQ0ZGE4OTBlMmI4YWZlNjVhYmNkYzhiMjNjNyJ9fX0="; + public static final String TEAM_GRAY = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZmNkYmJlYzJlN2M2Zjc5OTY2OTkyZThkM2I2OTAwZjk4ZGFhNGQ0ZGE4OTBlMmI4YWZlNjVhYmNkYzhiMjNjNyJ9fX0="; + public static final String TEAM_BLACK = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNjU3YjZlNGE3MmE5M2E3YTdhMjFmYTI4NmYyZWU3MTViNDlhNmJhNzgyNGRjNTMzZWQyNDBiOTYwNzExNDEzZSJ9fX0="; +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/common/Style.java b/core/src/main/java/zone/themcgamer/core/common/Style.java new file mode 100644 index 0000000..9b649a3 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/common/Style.java @@ -0,0 +1,78 @@ +package zone.themcgamer.core.common; + +import org.bukkit.ChatColor; +import zone.themcgamer.data.Rank; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * @author Braydon + */ +public class Style { + /** + * Return the rank required message for the given player name with the given prefix + * @return the rank required message. + * @param rank the rank that is required + */ + public static String rankRequired(Rank rank) { + if (rank.getCategory() == Rank.RankCategory.DONATOR) + return Style.color("\n" + + "&a&l Account &8∙ &cYou do not have " + rank.getDisplayName() + " rank!\n" + + "&7 You need " + rank.getColor() + "&l" + rank.getDisplayName() + "&7 or a &e&lhigher &7donator rank to unlock this!\n" + + "&b &nstore.mcgamerzone.net&f\n &7"); + return Style.main("Account", "You need " + rank.getColor() + rank.getDisplayName() + " &7or &chigher &7to use this command!"); + } + + /** + * Return the invalid account error for the given player name with the given prefix + * @param prefix the prefix + * @param playerName the player name + * @return the error + */ + public static String invalidAccount(String prefix, String playerName) { + return Style.error(prefix, "§7Could not find a Minecraft account with the name §b" + playerName); + } + + /** + * Return the default chat format with the given prefix and message + * @param prefix the prefix of the message + * @param message the message + * @return the formatted message + */ + public static String main(String prefix, String message) { + return color("&a&l" + prefix + " &8» &7" + message); + } + + /** + * Return the default error chat format with the given prefix and message + * @param prefix the prefix of the message + * @param message the message + * @return the formatted message + */ + public static String error(String prefix, String message) { + return color("&c&l" + prefix + " &8» &7" + message); + } + + /** + * Color the provided {@link Collection} using {@link ChatColor} + * @param lines the lines to color + * @return the colored lines + */ + public static List colorLines(Collection lines) { + List newLines = new ArrayList<>(); + for (String line : lines) + newLines.add(color(line)); + return newLines; + } + + /** + * Color the provided message using {@link ChatColor} + * @param message the message to color + * @return the colored message + */ + public static String color(String message) { + return ChatColor.translateAlternateColorCodes('&', message.replaceAll("§", "&")); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/common/WorldTime.java b/core/src/main/java/zone/themcgamer/core/common/WorldTime.java new file mode 100644 index 0000000..1bf23fa --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/common/WorldTime.java @@ -0,0 +1,31 @@ +package zone.themcgamer.core.common; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor @Getter +public enum WorldTime { + SUNRISE("Sunrise", new String[] { + "&7Beginning of the Minecraft day.", + "&7Villagers awaken and rise from their beds." + }, 24000L), + DAY("Day", new String[] { + "&7Villagers begin their workday." + }, 2000L), + NOON("Noon", new String[] { + "&7The sun is at its peak." + }, 6000L), + SUNSET("Sunset", new String[] { + "&7Villagers go to their beds and sleep." + }, 12000L), + NIGHT("Night", new String[] { + "&7First tick when monsters spawn outdoors in clear weather." + }, 13000L), + MIDNIGHT("Mid Night", new String[] { + "&7The moon is at its peak." + }, 18000L); + + private final String displayName; + private final String[] description; + private final long time; +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/common/WrappedBukkitEvent.java b/core/src/main/java/zone/themcgamer/core/common/WrappedBukkitEvent.java new file mode 100644 index 0000000..a295ab3 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/common/WrappedBukkitEvent.java @@ -0,0 +1,21 @@ +package zone.themcgamer.core.common; + +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; + +/** + * @author Braydon + * @implNote Easy to use wrapped Bukkit {@link Event}. + */ +public class WrappedBukkitEvent extends Event { + private static final HandlerList HANDLERS = new HandlerList(); + + @Override + public HandlerList getHandlers() { + return HANDLERS; + } + + public static HandlerList getHandlerList() { + return HANDLERS; + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/common/menu/Button.java b/core/src/main/java/zone/themcgamer/core/common/menu/Button.java new file mode 100644 index 0000000..9ade1e5 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/common/menu/Button.java @@ -0,0 +1,20 @@ +package zone.themcgamer.core.common.menu; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.ItemStack; + +import java.util.function.Consumer; + +/** + * This class holds the {@link ItemStack} and {@link Consumer} for + * the {@link InventoryClickEvent} for each button + * @author Braydon + */ +@RequiredArgsConstructor @AllArgsConstructor @Getter +public class Button { + private final ItemStack item; + private Consumer consumer; +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/common/menu/Menu.java b/core/src/main/java/zone/themcgamer/core/common/menu/Menu.java new file mode 100644 index 0000000..f695d62 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/common/menu/Menu.java @@ -0,0 +1,256 @@ +package zone.themcgamer.core.common.menu; + +import lombok.Getter; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +/** + * @author Braydon + */ +@Getter +public abstract class Menu { + /** + * A map of the menus opened for each player + */ + @Getter private static final Map menusMap = new HashMap<>(); + + protected final Player player; + private String title; + private final int size; + private final MenuType type; + private final Set flags = new HashSet<>(Collections.singletonList(MenuFlag.CLOSEABLE)); + protected Inventory inventory; + + /** + * The buttons for the menu (slot, button) + */ + private final Map buttonMap = new HashMap<>(); + + public Menu(Player player, String title, MenuType type) { + this(player, title, 1, type); + } + + public Menu(Player player, String title, int rows, MenuType type) { + this.player = player; + this.title = title; + if (title == null) + this.title = ""; + size = type == MenuType.CHEST ? rows * 9 : type.getInventoryType().getDefaultSize(); + if (type == MenuType.CHEST && rows > 6) + Bukkit.getLogger().info("Be cautious whilst using inventory sizes larger than 6 rows for type '" + type.name() + "', " + + "the menu may not display properly for some players"); + this.type = type; + setInventory(); + } + + /** + * Called when the menu is opened + */ + protected abstract void onOpen(); + + protected void setTitle(String title) { + this.title = title; + setInventory(); + } + + /** + * Open the menu to the player + */ + public void open() { + player.openInventory(inventory); + onOpen(); + if (this instanceof UpdatableMenu) + ((UpdatableMenu) this).onUpdate(); + player.updateInventory(); + menusMap.put(player, this); + } + + /** + * Close the player + */ + public void close() { + menusMap.remove(player); + player.closeInventory(); + } + + /** + * Add the defined flag to the menu + * @param flag - The flag to add + */ + protected void addFlag(MenuFlag flag) { + flags.add(flag); + } + + /** + * Get whether the menu has the defined flag + * @param flag - The flag to check + * @return whether the menu has the flag + */ + protected boolean hasFlag(MenuFlag flag) { + return flags.contains(flag); + } + + /** + * Remove the defined flag from the menu + * @param flag - The flag to remove + */ + protected void removeFlag(MenuFlag flag) { + flags.remove(flag); + } + + /** + * Fill the menu with the given button + * @param button - The button to fill the menu with + */ + protected void fill(Button button) { + for (int i = 0; i < size; i++) { + set(i, button); + } + } + + /** + * Fill the menu with the given button at the given slots. + * @see MenuPattern to get a list of slots + * @param slots - The slots to fill + * @param button - The button to fill the slots with + */ + protected void fill(List slots, Button button) { + for (Integer slot : slots) { + set(slot, button); + } + } + + /** + * Fill the borders of the menu with the given button + * @param button - The button to fill the borders with + * @author NoneTaken + */ + protected void fillBorders(Button button) { + for (int i = 0; i < size; i++) { + if (i < 10) + set(i, button); + if ((i % 9) == 0 && i != 9) + set(i, button); + if (i >= size - 9) + set(i, button); + if (i < 8) + continue; + if ((i / 8) - (i % 8) == 0) + set(i - 1, button); + } + } + + /** + * Fill the slots at the given column with the given button + * @param column - The column to fill + * @param button - The button to fill the column with + */ + protected void fillColumn(int column, Button button) { + for (int i = 0; i < size; i++) { + if (i % 9 == column) { + set(i, button); + } + } + } + + /** + * Fill the slots at the given row with the given button + * @param row - The row to fill + * @param button - The button to fill the row with + */ + protected void fillRow(int row, Button button) { + for (int i = 0; i < size; i++) { + if (i / 9 == row) { + set(i, button); + } + } + } + + /** + * Add a {@link Button} to the next available slot in the menu + * @param button the button to add + */ + public void add(Button button) { + for (int slot = 0; slot < size; slot++) { + if (get(slot) != null) + continue; + set(slot, button); + break; + } + } + + /** + * Set the slot at the given column and row to the given button + * @param column - The column + * @param row - The row + * @param button - The button to set + */ + protected void set(int column, int row, Button button) { + set((column * 9) + row, button); + } + + /** + * Set the given slot in the menu to the given button + * @param slot - The slot to set the button in + * @param button - The button + */ + protected void set(int slot, Button button) { + if (slot >= size || (type != MenuType.CHEST && slot >= type.getInventoryType().getDefaultSize())) + throw new ArrayIndexOutOfBoundsException("Slot must be inside of inventory for type '" + type.name() + "', default size=" + + type.getInventoryType().getDefaultSize()); + if (button == null) { + buttonMap.remove(slot); + inventory.setItem(slot, null); + return; + } + if (button.getItem().getType() == Material.AIR) + throw new IllegalArgumentException("Button item cannot be of type AIR"); + buttonMap.put(slot, button); + inventory.setItem(slot, button.getItem()); + } + + /** + * Get the button at the given column and row + * @param column - The column + * @param row - The row + * @return the button + */ + @Nullable + protected Button get(int column, int row) { + return get((column * 9) + row); + } + + /** + * Get the button at the given slot + * @param slot - The slot to get the button for + * @return the button + */ + @Nullable + protected Button get(int slot) { + return buttonMap.get(slot); + } + + /** + * Set the inventory field. This method is called when the menu + * is constructed or when the title is set + */ + private void setInventory() { + inventory = type == MenuType.CHEST ? Bukkit.createInventory(player, size, title) : + Bukkit.createInventory(player, type.getInventoryType(), title); + } + + /** + * Get the opened menu for the given player + * @param player - The player to get the menu for + * @return the menu + */ + @Nullable + public static Menu getOpenedMenu(Player player) { + return menusMap.get(player); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/common/menu/MenuFlag.java b/core/src/main/java/zone/themcgamer/core/common/menu/MenuFlag.java new file mode 100644 index 0000000..c9f9753 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/common/menu/MenuFlag.java @@ -0,0 +1,8 @@ +package zone.themcgamer.core.common.menu; + +/** + * @author Braydon + */ +public enum MenuFlag { + CLOSEABLE, DEBUG +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/common/menu/MenuManager.java b/core/src/main/java/zone/themcgamer/core/common/menu/MenuManager.java new file mode 100644 index 0000000..a08bda3 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/common/menu/MenuManager.java @@ -0,0 +1,119 @@ +package zone.themcgamer.core.common.menu; + +import org.bukkit.Bukkit; +import org.bukkit.entity.HumanEntity; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryAction; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryCloseEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.event.server.PluginDisableEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.java.JavaPlugin; +import zone.themcgamer.core.common.scheduler.ScheduleType; +import zone.themcgamer.core.common.scheduler.event.SchedulerEvent; + +/** + * @author Braydon + */ +public class MenuManager implements Listener { + private final JavaPlugin plugin; + + public MenuManager(JavaPlugin plugin) { + this.plugin = plugin; + Bukkit.getPluginManager().registerEvents(this, plugin); + } + + @EventHandler + private void onClick(InventoryClickEvent event) { + HumanEntity entity = event.getWhoClicked(); + if (entity instanceof Player) { + Player player = (Player) entity; + int slot = event.getRawSlot(); + InventoryAction action = event.getAction(); + Menu menu; + if (slot == -999 || action == InventoryAction.NOTHING || (menu = Menu.getOpenedMenu(player)) == null) + return; + if (menu.hasFlag(MenuFlag.DEBUG)) { + ItemStack item = event.getCurrentItem(); + ItemStack cursor = event.getCursor(); + player.sendMessage("slot=" + slot + " (type=" + event.getSlotType().name() + ")"); + player.sendMessage("action=" + action.name()); + player.sendMessage("clickType=" + event.getClick()); + player.sendMessage("item=" + (item == null ? "null" : item.toString())); + player.sendMessage("cursor=" + (cursor == null ? "null" : cursor.toString())); + } + if (event.isShiftClick()) { + event.setCancelled(true); + return; + } + if (!event.getClickedInventory().equals(menu.getInventory())) + return; + event.setCancelled(true); + Button button = menu.getButtonMap().get(slot); + if (button != null && (button.getConsumer() != null)) + button.getConsumer().accept(event); + } + } + + @EventHandler + private void onUpdate(SchedulerEvent event) { + if (event.getType() != ScheduleType.THREE_TICKS) + return; + for (Player player : Bukkit.getOnlinePlayers()) { + Menu menu = Menu.getOpenedMenu(player); + if (menu == null) + continue; + if (menu instanceof UpdatableMenu) { + UpdatableMenu updatableMenu = (UpdatableMenu) menu; + if ((System.currentTimeMillis() - updatableMenu.getLastUpdate()) >= updatableMenu.getDelay()) { + updatableMenu.onUpdate(); + updatableMenu.setLastUpdate(System.currentTimeMillis()); + player.updateInventory(); + } + } + } + } + + @EventHandler + private void onClose(InventoryCloseEvent event) { + HumanEntity entity = event.getPlayer(); + if (entity instanceof Player) { + Menu menu = Menu.getMenusMap().remove(entity); + if (menu != null) { + if (!menu.hasFlag(MenuFlag.CLOSEABLE)) { + Bukkit.getScheduler().scheduleSyncDelayedTask(plugin, menu::open, 1L); + if (menu.hasFlag(MenuFlag.DEBUG)) + Bukkit.broadcastMessage("Re-opening menu (" + menu.getTitle() + "§f) for player " + entity.getName() + ", the menu is not closeable"); + } else { + if (menu.hasFlag(MenuFlag.DEBUG)) + Bukkit.broadcastMessage("Removing " + entity.getName() + " from menu map, they closed the menu (" + menu.getTitle() + "§f)"); + } + } + } + } + + @EventHandler + private void onQuit(PlayerQuitEvent event) { + Player player = event.getPlayer(); + Menu menu = Menu.getMenusMap().remove(player); + if (menu != null && (menu.hasFlag(MenuFlag.DEBUG))) + Bukkit.broadcastMessage("Removing " + player.getName() + " from menu map, they left the server (menu: " + menu.getTitle() + "§f)"); + } + + @EventHandler + private void onDisable(PluginDisableEvent event) { + Plugin plugin = event.getPlugin(); + if (plugin.equals(this.plugin)) { + for (Menu menu : Menu.getMenusMap().values()) { + if (menu.hasFlag(MenuFlag.DEBUG)) + Bukkit.broadcastMessage("Closing menu (" + menu.getTitle() + "§f) for " + menu.getPlayer().getName() + " due to plugin disable: " + plugin.getName()); + menu.close(); + } + Menu.getMenusMap().clear(); + } + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/common/menu/MenuPattern.java b/core/src/main/java/zone/themcgamer/core/common/menu/MenuPattern.java new file mode 100644 index 0000000..ebcf42c --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/common/menu/MenuPattern.java @@ -0,0 +1,36 @@ +package zone.themcgamer.core.common.menu; + +import lombok.experimental.UtilityClass; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Braydon + */ +@UtilityClass +public class MenuPattern { + /** + * Get the slots matching the given pattern. + * Example: 'XXOOOOOXX' would return slots + * from 1 to 6 for that pattern + * @param patterns - The patterns + * @return the slots + */ + public static List getSlots(String... patterns) { + List slots = new ArrayList<>(); + for (int row = 0; row < patterns.length; row++) { + String s = patterns[row]; + if (s.length() != 9) + throw new IllegalArgumentException("String '" + s + "' cannot have a length of " + s.length() + ", it must be 9!"); + char[] chars = s.toCharArray(); + for (int slot = 0; slot < 9; slot++) { + char character = chars[slot]; + if (Character.toLowerCase(character) == 'o') { + slots.add((row * 9) + slot); + } + } + } + return slots; + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/common/menu/MenuType.java b/core/src/main/java/zone/themcgamer/core/common/menu/MenuType.java new file mode 100644 index 0000000..37ecaa7 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/common/menu/MenuType.java @@ -0,0 +1,18 @@ +package zone.themcgamer.core.common.menu; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.bukkit.event.inventory.InventoryType; + +/** + * @author Braydon + */ +@AllArgsConstructor @Getter +public enum MenuType { + CHEST(InventoryType.CHEST), + DISPENSER(InventoryType.DISPENSER), + FURNACE(InventoryType.FURNACE), + HOPPER(InventoryType.HOPPER); + + private final InventoryType inventoryType; +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/common/menu/PaginatedMenu.java b/core/src/main/java/zone/themcgamer/core/common/menu/PaginatedMenu.java new file mode 100644 index 0000000..7fdd826 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/common/menu/PaginatedMenu.java @@ -0,0 +1,55 @@ +package zone.themcgamer.core.common.menu; + +import lombok.Getter; +import org.bukkit.entity.Player; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author Braydon + */ +@Getter +public abstract class PaginatedMenu extends Menu { + private final int page; + private final List input; + private final int itemsPerPage; + private final Map contents = new HashMap<>(); + + public PaginatedMenu(Player player, String title, MenuType type, int page, List input, int itemsPerPage) { + super(player, title, type); + this.page = page; + this.input = input; + this.itemsPerPage = itemsPerPage; + populateContents(itemsPerPage); + } + + public PaginatedMenu(Player player, String title, int rows, MenuType type, int page, List input, int itemsPerPage) { + super(player, title, rows, type); + this.page = page; + this.input = input; + this.itemsPerPage = itemsPerPage; + populateContents(itemsPerPage); + } + + /** + * Calculate and return what the max page is + * @return the max page + */ + protected int getMaxPage() { + int maxPage = (int) Math.ceil((double) input.size() / itemsPerPage); + return maxPage <= 0 ? 1 : maxPage; + } + + /** + * Populate the contents map with the position and value + * @param itemsPerPage - The amount of items to display per page + */ + private void populateContents(int itemsPerPage) { + for (int i = itemsPerPage * (page - 1); i < itemsPerPage * page && i < input.size(); i++) + contents.put(i + 1, input.get(i)); + if (hasFlag(MenuFlag.DEBUG)) + player.sendMessage("input=" + input.size() + ", contents for page=" + contents.size()); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/common/menu/UpdatableMenu.java b/core/src/main/java/zone/themcgamer/core/common/menu/UpdatableMenu.java new file mode 100644 index 0000000..e1bb296 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/common/menu/UpdatableMenu.java @@ -0,0 +1,50 @@ +package zone.themcgamer.core.common.menu; + +import lombok.Getter; +import org.bukkit.entity.Player; + +import java.util.concurrent.TimeUnit; + +/** + * @author Braydon + */ +@Getter +public abstract class UpdatableMenu extends Menu { + private static final long DEFAULT_TIME = TimeUnit.SECONDS.toMillis(2L); + + private final long delay; + private long lastUpdate; + + public UpdatableMenu(Player player, String title, MenuType type) { + this(player, title, type, DEFAULT_TIME); + } + + public UpdatableMenu(Player player, String title, MenuType type, long delay) { + super(player, title, type); + this.delay = delay; + } + + public UpdatableMenu(Player player, String title, int rows, MenuType type) { + this(player, title, rows, type, DEFAULT_TIME); + } + + public UpdatableMenu(Player player, String title, int rows, MenuType type, long delay) { + super(player, title, rows, type); + this.delay = delay; + } + + /** + * Called when the menu is updated + */ + public abstract void onUpdate(); + + /** + * This method is optional for this menu type + */ + @Override + protected void onOpen() {} + + protected void setLastUpdate(long lastUpdate) { + this.lastUpdate = lastUpdate; + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/common/scheduler/ScheduleType.java b/core/src/main/java/zone/themcgamer/core/common/scheduler/ScheduleType.java new file mode 100644 index 0000000..8501de2 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/common/scheduler/ScheduleType.java @@ -0,0 +1,55 @@ +package zone.themcgamer.core.common.scheduler; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.concurrent.TimeUnit; + +/** + * @author Braydon + */ +@RequiredArgsConstructor @Getter +public enum ScheduleType { + // Hours + TEN_HOURS(TimeUnit.HOURS.toMillis(10L)), + FIVE_HOURS(TimeUnit.HOURS.toMillis(5L)), + THREE_HOURS(TimeUnit.HOURS.toMillis(3L)), + HOUR(TimeUnit.HOURS.toMillis(1L)), + + // Minutes + THIRTY_MINUTES(TimeUnit.MINUTES.toMillis(30L)), + TWENTY_MINUTES(TimeUnit.MINUTES.toMillis(20L)), + FIFTEEN_MINUTES(TimeUnit.MINUTES.toMillis(15L)), + TEN_MINUTES(TimeUnit.MINUTES.toMillis(10L)), + FIVE_MINUTES(TimeUnit.MINUTES.toMillis(5L)), + THREE_MINUTES(TimeUnit.MINUTES.toMillis(3L)), + MINUTE(TimeUnit.MINUTES.toMillis(1L)), + + // Seconds + THIRTY_SECONDS(TimeUnit.SECONDS.toMillis(30L)), + TWENTY_SECONDS(TimeUnit.SECONDS.toMillis(20L)), + FIFTEEN_SECONDS(TimeUnit.SECONDS.toMillis(15L)), + TEN_SECONDS(TimeUnit.SECONDS.toMillis(10L)), + FIVE_SECONDS(TimeUnit.SECONDS.toMillis(5L)), + THREE_SECONDS(TimeUnit.SECONDS.toMillis(3L)), + SECOND(TimeUnit.SECONDS.toMillis(1L)), + HALF_SECOND(500L), + + // Ticks (a tick is 50ms, so we multiply each tick by 50) + THIRTY_TICKS(30L * 50L), + TWENTY_TICKS(20L * 50L), + FIFTEEN_TICKS(15L * 50L), + TEN_TICKS(10L * 50L), + FIVE_TICKS(5L * 50L), + THREE_TICKS(3L * 50L), + TICK(50L); + + private final long time; + private boolean firstRun = true; + private long lastRun; + + public void run() { + firstRun = false; + lastRun = System.currentTimeMillis(); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/common/scheduler/Scheduler.java b/core/src/main/java/zone/themcgamer/core/common/scheduler/Scheduler.java new file mode 100644 index 0000000..000cee0 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/common/scheduler/Scheduler.java @@ -0,0 +1,30 @@ +package zone.themcgamer.core.common.scheduler; + +import org.bukkit.Bukkit; +import org.bukkit.plugin.java.JavaPlugin; +import zone.themcgamer.core.common.scheduler.event.SchedulerEvent; + +/** + * @author Braydon + */ +public class Scheduler { + /** + * This will loop through all scheduler types and check when the last + * elapsed time was for that type. If the elapsed time is greater or equal + * to the time specified in the type, then we will call the {@code SchedulerEvent} + * for that type and run the {@code scheduleType.run()} method. The benefits of this + * is so you don't need to create schedulers to say run something every x amount of + * time, you can simply just listen to this event. This will also help with performance + * so you don't need to create multiple schedulers and you can run everything off of one + */ + public Scheduler(JavaPlugin plugin) { + Bukkit.getScheduler().runTaskTimer(plugin, () -> { + for (ScheduleType scheduleType : ScheduleType.values()) { + if ((System.currentTimeMillis() - scheduleType.getLastRun()) < scheduleType.getTime()) + continue; + Bukkit.getPluginManager().callEvent(new SchedulerEvent(scheduleType)); + scheduleType.run(); + } + }, 1L, 1L); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/common/scheduler/event/SchedulerEvent.java b/core/src/main/java/zone/themcgamer/core/common/scheduler/event/SchedulerEvent.java new file mode 100644 index 0000000..dee1079 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/common/scheduler/event/SchedulerEvent.java @@ -0,0 +1,14 @@ +package zone.themcgamer.core.common.scheduler.event; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import zone.themcgamer.core.common.WrappedBukkitEvent; +import zone.themcgamer.core.common.scheduler.ScheduleType; + +/** + * @author Braydon + */ +@AllArgsConstructor @Getter +public class SchedulerEvent extends WrappedBukkitEvent { + private final ScheduleType type; +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/common/scoreboard/ScoreboardEntry.java b/core/src/main/java/zone/themcgamer/core/common/scoreboard/ScoreboardEntry.java new file mode 100644 index 0000000..681d8e2 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/common/scoreboard/ScoreboardEntry.java @@ -0,0 +1,90 @@ +package zone.themcgamer.core.common.scoreboard; + +import lombok.Getter; +import lombok.Setter; +import org.bukkit.ChatColor; +import org.bukkit.scoreboard.Scoreboard; +import org.bukkit.scoreboard.Team; + +/** + * @author Braydon + */ +public class ScoreboardEntry { + @Getter private final ScoreboardProvider provider; + @Setter @Getter private String text; + private String identifier; + private Team team; + + /** + * Creates a new scoreboard line with the provided text for the + * provided scoreboard provider + * @param provider - The scoreboard provider you would like to make the line for + * @param text - The text you would like to be displayed for this line + */ + public ScoreboardEntry(ScoreboardProvider provider, String text) { + this.provider = provider; + this.text = text; + this.identifier = provider.getTeamName(); + createTeam(); + } + + /** + * Create the team for the line + */ + protected void createTeam() { + Scoreboard scoreboard = provider.getScoreboard(); + if (scoreboard == null) + return; + String teamName = identifier; + if (teamName.length() > 16) + teamName = teamName.substring(0, 16); + Team team = scoreboard.getTeam(teamName); + if (team == null) + team = scoreboard.registerNewTeam(teamName); + if (!team.getEntries().contains(identifier)) + team.addEntry(identifier); + else { + identifier = provider.getTeamName(); + createTeam(); + return; + } + if (!provider.getEntries().contains(this)) + provider.getEntries().add(this); + this.team = team; + } + + /** + * Display the line at the provided position + * @param position - The position you would like to display the line at + */ + protected void display(int position) { + if (text.length() > 16) { + String prefix = text.substring(0, 16); + String suffix; + + if (prefix.charAt(15) == ChatColor.COLOR_CHAR) { + prefix = prefix.substring(0, 15); + suffix = text.substring(15); + } else if (prefix.charAt(14) == ChatColor.COLOR_CHAR) { + prefix = prefix.substring(0, 14); + suffix = text.substring(14); + } else suffix = ChatColor.getLastColors(prefix) + text.substring(16); + if (suffix.length() > 16) + suffix = suffix.substring(0, 16); + team.setPrefix(prefix); + team.setSuffix(suffix); + } else { + team.setPrefix(text); + team.setSuffix(""); + } + provider.getObjective().getScore(identifier).setScore(position); + } + + /** + * Remove the line + */ + protected void remove() { + provider.getIdentifiers().remove(identifier); + provider.getScoreboard().resetScores(identifier); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/common/scoreboard/ScoreboardHandler.java b/core/src/main/java/zone/themcgamer/core/common/scoreboard/ScoreboardHandler.java new file mode 100644 index 0000000..957fe1a --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/common/scoreboard/ScoreboardHandler.java @@ -0,0 +1,161 @@ +package zone.themcgamer.core.common.scoreboard; + +import lombok.Getter; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.event.server.PluginDisableEvent; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.scoreboard.Objective; +import zone.themcgamer.core.common.Style; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @author Braydon + */ +@Getter +public class ScoreboardHandler implements Listener { + @Getter private static ScoreboardHandler instance; + + private final JavaPlugin plugin; + + private final Class boardClass; + private Thread thread; + private final long delay; + private boolean running; + + private final Map boards = new ConcurrentHashMap<>(); + + public ScoreboardHandler(JavaPlugin plugin, Class boardClass, long delay) { + instance = this; + this.plugin = plugin; + this.boardClass = boardClass; + this.delay = delay; + running = true; + + (thread = new Thread("Scoreboard Thread") { + @Override + public void run() { + while (running) { + for (Player player : Bukkit.getOnlinePlayers()) { + ScoreboardProvider provider = boards.get(player); + if (provider == null) + continue; + Objective objective = provider.getObjective(); + String title = provider.getTitle(); + + if (!objective.getDisplayName().equals(title)) + objective.setDisplayName(title); + + List lines = provider.getLines(); + if (lines == null || (lines.isEmpty())) { + provider.getEntries().forEach(ScoreboardEntry::remove); + provider.getEntries().clear(); + } else { + Collections.reverse(lines); + if (provider.getEntries().size() > lines.size()) { + for (int i = lines.size(); i < provider.getEntries().size(); i++) { + ScoreboardEntry entry = provider.getEntryAtPosition(i); + if (entry != null) + entry.remove(); + } + } + for (int i = 0; i < lines.size(); i++) { + ScoreboardEntry entry = provider.getEntryAtPosition(i); + String line = Style.color(lines.get(i)); + int lineNumber = i + 1; + if (entry == null) { + entry = new ScoreboardEntry(provider, line); + entry.setText(line); + entry.display(lineNumber); + } else { + if (!entry.getText().equals(line)) { + entry.setText(line); + entry.display(lineNumber); + } + } + } + } + } + try { + sleep(delay * 50L); + } catch (InterruptedException ex) { + ex.printStackTrace(); + } + } + } + }).start(); + for (Player player : Bukkit.getOnlinePlayers()) + giveBoard(player); + Bukkit.getPluginManager().registerEvents(this, plugin); + } + + /** + * When a player joins the server, we wanna give them + * the scoreboard + */ + @EventHandler + private void onJoin(PlayerJoinEvent event) { + giveBoard(event.getPlayer()); + } + + /** + * When a player leaves the server, we wanna remove the + * scoreboard from them + */ + @EventHandler + private void onQuit(PlayerQuitEvent event) { + removeBoard(event.getPlayer()); + } + + /** + * When the plugin is disabled, we wanna stop the + * scoreboard thread and remove the scoreboard from + * all online players + */ + @EventHandler + private void onDisable(PluginDisableEvent event) { + if (event.getPlugin().equals(plugin)) { + if (thread != null) { + thread.stop(); + thread = null; + } + running = false; + for (Player player : Bukkit.getOnlinePlayers()) + removeBoard(player); + } + } + + /** + * Give the provided player the scoreboard + * @param player - The player you would like to give the scoreboard to + */ + public void giveBoard(Player player) { + if (boards.containsKey(player)) + throw new IllegalStateException("Player '" + player.getName() + "' already has the scoreboard"); + try { + ScoreboardProvider provider = (ScoreboardProvider) boardClass.getConstructors()[0].newInstance(player); + boards.put(player, provider); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + + /** + * Remove the scoreboard from the provided player + * @param player - The player you would like to remove the scoreboard from + */ + public void removeBoard(Player player) { + if (!boards.containsKey(player)) + throw new IllegalStateException("Player '" + player.getName() + "' does not have the scoreboard"); + player.setScoreboard(Bukkit.getScoreboardManager().getMainScoreboard()); + boards.remove(player); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/common/scoreboard/ScoreboardProvider.java b/core/src/main/java/zone/themcgamer/core/common/scoreboard/ScoreboardProvider.java new file mode 100644 index 0000000..7609c4a --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/common/scoreboard/ScoreboardProvider.java @@ -0,0 +1,68 @@ +package zone.themcgamer.core.common.scoreboard; + +import lombok.Getter; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.scoreboard.DisplaySlot; +import org.bukkit.scoreboard.Objective; +import org.bukkit.scoreboard.Scoreboard; +import zone.themcgamer.core.common.HiddenStringUtils; + +import java.util.*; + +/** + * @author Braydon + */ +@Getter +public abstract class ScoreboardProvider { + protected final Player player; + private final Set identifiers = new HashSet<>(); + private final List entries = new ArrayList<>(); + + private Scoreboard scoreboard; + private Objective objective; + + public ScoreboardProvider(Player player) { + this.player = player; + give(); + } + + public abstract String getTitle(); + public abstract List getLines(); + + /** + * Get an entry at the provided index + * @param index - The index of the entry you would like to get + * @return the entry at the provided index, null if none + */ + protected ScoreboardEntry getEntryAtPosition(int index) { + if (index < 0 || index >= entries.size()) + return null; + return entries.get(index); + } + + /** + * Get a unique name for a team + * @return the unique name for a team + */ + public String getTeamName() { + String identifier = UUID.randomUUID().toString().replaceAll("-", ""); + identifier = HiddenStringUtils.encode(identifier).substring(0, 16); + identifiers.add(identifier); + return identifier; + } + + /** + * Give the player the scoreboard + */ + private void give() { + if (player.getScoreboard().equals(Bukkit.getScoreboardManager().getMainScoreboard())) + scoreboard = Bukkit.getScoreboardManager().getNewScoreboard(); + else scoreboard = player.getScoreboard(); + if (scoreboard.getObjective("KauriBoard") == null) + objective = scoreboard.registerNewObjective("KauriBoard", "dummy"); + objective.setDisplaySlot(DisplaySlot.SIDEBAR); + objective.setDisplayName(getTitle()); + player.setScoreboard(scoreboard); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/common/scoreboard/WritableScoreboard.java b/core/src/main/java/zone/themcgamer/core/common/scoreboard/WritableScoreboard.java new file mode 100644 index 0000000..9a8687d --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/common/scoreboard/WritableScoreboard.java @@ -0,0 +1,39 @@ +package zone.themcgamer.core.common.scoreboard; + +import org.bukkit.ChatColor; +import org.bukkit.entity.Player; +import zone.themcgamer.common.RandomUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Braydon + */ +public abstract class WritableScoreboard extends ScoreboardProvider { + private final List lines = new ArrayList<>(); + + public WritableScoreboard(Player player) { + super(player); + } + + public abstract void writeLines(); + + @Override + public List getLines() { + lines.clear(); + writeLines(); + return lines; + } + + protected void write(Object object) { + lines.add(object.toString()); + } + + protected void writeBlank() { + ChatColor color = RandomUtils.random(ChatColor.class); + if (color == null) + color = ChatColor.RESET; + lines.add(color.toString()); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/cooldown/Cooldown.java b/core/src/main/java/zone/themcgamer/core/cooldown/Cooldown.java new file mode 100644 index 0000000..32b9aba --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/cooldown/Cooldown.java @@ -0,0 +1,18 @@ +package zone.themcgamer.core.cooldown; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author Braydon + */ +@AllArgsConstructor @Getter +public class Cooldown { + private final String name; + private final long time, started; + private final boolean inform; + + public long getRemaining() { + return (started + time) - System.currentTimeMillis(); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/cooldown/CooldownHandler.java b/core/src/main/java/zone/themcgamer/core/cooldown/CooldownHandler.java new file mode 100644 index 0000000..f66f8e8 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/cooldown/CooldownHandler.java @@ -0,0 +1,69 @@ +package zone.themcgamer.core.cooldown; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.plugin.java.JavaPlugin; +import zone.themcgamer.common.TimeUtils; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.core.common.scheduler.ScheduleType; +import zone.themcgamer.core.common.scheduler.event.SchedulerEvent; +import zone.themcgamer.core.module.Module; +import zone.themcgamer.core.module.ModuleInfo; + +import java.util.*; + +/** + * @author Braydon + */ +@ModuleInfo(name = "Cooldowns") +public class CooldownHandler extends Module { + private static final Map> cooldowns = new HashMap<>(); + + public CooldownHandler(JavaPlugin plugin) { + super(plugin); + } + + @EventHandler + private void expireCooldowns(SchedulerEvent event) { + if (event.getType() != ScheduleType.TICK) + return; + for (Player player : Bukkit.getOnlinePlayers()) { + List cooldowns = getCooldowns(player); + cooldowns.removeIf(cooldown -> { + if (cooldown.getRemaining() > 0) + return false; + if (cooldown.isInform()) + player.sendMessage(Style.main("Cooldown", "Your cooldown for §f" + cooldown.getName() + " §7has expired")); + return true; + }); + } + } + + @EventHandler + private void onQuit(PlayerQuitEvent event) { + cooldowns.remove(event.getPlayer()); + } + + public static boolean canUse(Player player, String name, long time, boolean inform) { + List cooldowns = getCooldowns(player); + Optional optionalCooldown = cooldowns.stream() + .filter(cooldown -> cooldown.getName().equalsIgnoreCase(name) && cooldown.getRemaining() > 0) + .findFirst(); + if (optionalCooldown.isPresent()) { + if (inform) { + player.sendMessage(Style.error("Cooldown", "§f" + name + " §cis still on cooldown for another §f" + + TimeUtils.convertString(optionalCooldown.get().getRemaining()))); + } + return false; + } + cooldowns.add(new Cooldown(name, time, System.currentTimeMillis(), inform)); + CooldownHandler.cooldowns.put(player, cooldowns); + return true; + } + + private static List getCooldowns(Player player) { + return cooldowns.getOrDefault(player, new ArrayList<>()); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/game/GameCategory.java b/core/src/main/java/zone/themcgamer/core/game/GameCategory.java new file mode 100644 index 0000000..f504d19 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/game/GameCategory.java @@ -0,0 +1,15 @@ +package zone.themcgamer.core.game; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor @Getter +public enum GameCategory { + SURVIVE("§aSurvive"), + MINIGAME("§2Minigame"), + PVP("§cPvP"), + FREEBUILD("§bFree Build"), + COMPETITIVE("§3Competitive"); + + private final String name; +} diff --git a/core/src/main/java/zone/themcgamer/core/game/MGZGame.java b/core/src/main/java/zone/themcgamer/core/game/MGZGame.java new file mode 100644 index 0000000..cb6492f --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/game/MGZGame.java @@ -0,0 +1,136 @@ +package zone.themcgamer.core.game; + +import com.cryptomorin.xseries.XMaterial; +import lombok.AllArgsConstructor; +import lombok.Getter; +import zone.themcgamer.core.game.kit.KitDisplay; +import zone.themcgamer.core.game.kit.impl.WarriorKit; +import zone.themcgamer.core.world.WorldCategory; +import zone.themcgamer.data.jedis.data.ServerGroup; +import zone.themcgamer.data.jedis.data.server.MinecraftServer; +import zone.themcgamer.data.jedis.repository.RedisRepository; +import zone.themcgamer.data.jedis.repository.impl.ServerGroupRepository; + +import java.util.*; + +/** + * @author Braydon + * @implNote This represents a game on the server and its properties + */ +@AllArgsConstructor @Getter +public enum MGZGame { + SKYBLOCK("Skyblock Pirate", new String[] { + "Spawn on a floating island with", + "limited resources. &bExpand and grow&7!", + "", + "&c&oAbove all don't fall!", + "", + "&a&lFEATURES", + " &e» &7Custom Islands", + " &e» &7McMMO", + " &e» &7Minions", + " &e» &7Player Shops", + " &e» &7Player Warps", + " &e» &7Economy" + }, new WorldCategory[] { + WorldCategory.SKYBLOCK + }, WorldCategory.SKYBLOCK.getIcon(), GameCategory.SURVIVE, "Skyblock", 0, 200, null), + + PRISON("Prison", new String[] { + "Start mining and rank-up to the", + "highest level and prestige up!", + "", + "&a&lFEATURES", + " &e» &7Economy" + }, new WorldCategory[] { + WorldCategory.PRISON + }, WorldCategory.PRISON.getIcon(), GameCategory.SURVIVE, "Prison", 0, 200, null), + + ARCADE("Arcade", new String[] { + "Arcade desc" + }, new WorldCategory[] { + WorldCategory.THE_BRIDGE, + WorldCategory.CHAOSPVP, + WorldCategory.DISASTERS + }, XMaterial.NOTE_BLOCK, GameCategory.MINIGAME,"Arcade", -1, -1, null), + + THE_BRIDGE("The Bridge", new String[] { + "Battle it out and destroy each", + "others nexus to earn the top spot!" + }, new WorldCategory[] { + WorldCategory.THE_BRIDGE + }, WorldCategory.THE_BRIDGE.getIcon(), GameCategory.MINIGAME, "TheBridge", 2, 32, new KitDisplay[] { + new WarriorKit() + }), + + CHAOSPVP("ChaosPvP", new String[] { + "An arena of chaos, gear up", + "and survive on this battlefield,", + "alone or with your friends!" + }, new WorldCategory[] { + WorldCategory.CHAOSPVP + }, WorldCategory.CHAOSPVP.getIcon(), GameCategory.PVP,"ChaosPvP", 6, 32, null), + + DISASTERS("Disasters", new String[] { + "Survive in a world where various", + "disasters will come!" + }, new WorldCategory[] { + WorldCategory.DISASTERS + }, WorldCategory.DISASTERS.getIcon(), GameCategory.MINIGAME,"Disasters", 6, 32, null); + + private final String name; + private final String[] description; + private final WorldCategory[] worldCategories; + private final XMaterial icon; + private final GameCategory gameCategory; + private final String serverGroup; + private final int minPlayers, maxPlayers; + private final KitDisplay[] kitDisplays; + + public KitDisplay getKitDisplay(String name) { + if (kitDisplays == null) + return null; + return Arrays.stream(kitDisplays) + .filter(kitDisplay -> kitDisplay.getClass().getSimpleName().equalsIgnoreCase(name) || kitDisplay.getId().equalsIgnoreCase(name)) + .findFirst().orElse(null); + } + + /** + * Get the amount of players playing this game + * @return the amount of players playing + */ + public int getPlaying() { + int players = 0; + Optional optionalServerGroupRepository = RedisRepository.getRepository(ServerGroupRepository.class); + if (optionalServerGroupRepository.isPresent()) { + Optional optionalServerGroup = optionalServerGroupRepository.get().lookup(serverGroup); + if (optionalServerGroup.isPresent()) { + for (MinecraftServer server : optionalServerGroup.get().getServers()) { + players+= server.getOnline(); + } + } + } + return players; + } + + /** + * Get the best {@link MinecraftServer} to join for this game + * @return the optional server + */ + public Optional getBestServer() { + Optional optionalServerGroupRepository = RedisRepository.getRepository(ServerGroupRepository.class); + if (optionalServerGroupRepository.isEmpty()) + return Optional.empty(); + ServerGroupRepository serverGroupRepository = optionalServerGroupRepository.get(); + Optional optionalServerGroup = serverGroupRepository.lookup(serverGroup); + if (optionalServerGroup.isEmpty()) + return Optional.empty(); + List servers = new ArrayList<>(optionalServerGroup.get().getServers()); + servers.removeIf(minecraftServer -> !minecraftServer.isRunning()); + servers.sort((a, b) -> Integer.compare(b.getOnline(), a.getOnline())); + if (servers.isEmpty()) + return Optional.empty(); + Collections.shuffle(servers); + return Optional.of(servers.get(0)); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/game/kit/KitClient.java b/core/src/main/java/zone/themcgamer/core/game/kit/KitClient.java new file mode 100644 index 0000000..d24adc7 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/game/kit/KitClient.java @@ -0,0 +1,22 @@ +package zone.themcgamer.core.game.kit; + +import lombok.Getter; +import zone.themcgamer.core.game.MGZGame; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author Braydon + */ +@Getter +public class KitClient { + private final Map selectedKit = new HashMap<>(); + + public KitDisplay getKit(MGZGame game) { + KitDisplay kitDisplay = selectedKit.get(game); + if (kitDisplay == null && (game.getKitDisplays().length > 0)) + kitDisplay = game.getKitDisplays()[0]; + return kitDisplay; + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/game/kit/KitDisplay.java b/core/src/main/java/zone/themcgamer/core/game/kit/KitDisplay.java new file mode 100644 index 0000000..248facb --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/game/kit/KitDisplay.java @@ -0,0 +1,15 @@ +package zone.themcgamer.core.game.kit; + +import com.cryptomorin.xseries.XMaterial; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author Braydon + */ +@AllArgsConstructor @Getter +public class KitDisplay { + private final String id, name; + private final String[] description; + private final XMaterial icon; +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/game/kit/KitManager.java b/core/src/main/java/zone/themcgamer/core/game/kit/KitManager.java new file mode 100644 index 0000000..62186ac --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/game/kit/KitManager.java @@ -0,0 +1,48 @@ +package zone.themcgamer.core.game.kit; + +import org.bukkit.plugin.java.JavaPlugin; +import zone.themcgamer.common.EnumUtils; +import zone.themcgamer.core.account.MiniAccount; +import zone.themcgamer.core.game.MGZGame; +import zone.themcgamer.core.module.ModuleInfo; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Optional; +import java.util.UUID; + +/** + * @author Braydon + */ +@ModuleInfo(name = "Kit Manager") +public class KitManager extends MiniAccount { + public KitManager(JavaPlugin plugin) { + super(plugin); + } + + @Override + public KitClient getAccount(int accountId, UUID uuid, String name, String ip, String encryptedIp) { + return new KitClient(); + } + + @Override + public String getQuery(int accountId, UUID uuid, String name, String ip, String encryptedIp) { + return "SELECT game, kit FROM `kits` WHERE `accountId` = '" + accountId + "';"; + } + + @Override + public void loadAccount(int accountId, UUID uuid, String name, String ip, String encryptedIp, ResultSet resultSet) throws SQLException { + Optional client = lookup(uuid); + if (!client.isPresent()) + return; + while (resultSet.next()) { + MGZGame game = EnumUtils.fromString(MGZGame.class, resultSet.getString("game")); + if (game == null) + return; + KitDisplay kitDisplay = game.getKitDisplay(resultSet.getString("kit")); + if (kitDisplay == null) + return; + client.get().getSelectedKit().put(game, kitDisplay); + } + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/game/kit/impl/WarriorKit.java b/core/src/main/java/zone/themcgamer/core/game/kit/impl/WarriorKit.java new file mode 100644 index 0000000..f9a39c0 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/game/kit/impl/WarriorKit.java @@ -0,0 +1,21 @@ +package zone.themcgamer.core.game.kit.impl; + +import com.cryptomorin.xseries.XMaterial; +import zone.themcgamer.core.game.kit.KitDisplay; + +/** + * @author Braydon + */ +public class WarriorKit extends KitDisplay { + public WarriorKit() { + super("warrior", "Warrior", new String[] { + "&6&lItems", + "&b▸ &7Stone Sword", + "&b▸ &7Wooden Pickaxe", + "&b▸ &7Wooden Axe", + "&b▸ &7Wooden Shovel", + "&b▸ &7Crafting Table", + "&b▸ &7Shears" + }, XMaterial.WOODEN_SWORD); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/kingdom/KingdomManager.java b/core/src/main/java/zone/themcgamer/core/kingdom/KingdomManager.java new file mode 100644 index 0000000..aaedbc2 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/kingdom/KingdomManager.java @@ -0,0 +1,93 @@ +package zone.themcgamer.core.kingdom; + +import org.bukkit.entity.Player; +import org.bukkit.plugin.java.JavaPlugin; +import zone.themcgamer.core.account.Account; +import zone.themcgamer.core.account.AccountManager; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.core.game.MGZGame; +import zone.themcgamer.core.kingdom.command.KingdomCommand; +import zone.themcgamer.core.module.Module; +import zone.themcgamer.core.module.ModuleInfo; +import zone.themcgamer.core.plugin.MGZPlugin; +import zone.themcgamer.core.traveller.ServerTraveller; +import zone.themcgamer.data.Rank; +import zone.themcgamer.data.jedis.data.ServerGroup; +import zone.themcgamer.data.jedis.repository.RedisRepository; +import zone.themcgamer.data.jedis.repository.impl.ServerGroupRepository; + +import java.util.Optional; + +/** + * @author Braydon + */ +@ModuleInfo(name = "Kingdoms") +public class KingdomManager extends Module { + private static final int MAX_KINGDOMS = 3; + private static final MGZGame DEFAULT_GAME = MGZGame.THE_BRIDGE; + + private final ServerGroupRepository serverGroupRepository; + + public KingdomManager(JavaPlugin plugin, ServerTraveller traveller) { + super(plugin); + serverGroupRepository = RedisRepository.getRepository(ServerGroupRepository.class).orElse(null); +// JedisCommandHandler.getInstance().addListener(jedisCommand -> { +// if (jedisCommand instanceof ServerStateChangeCommand) { +// ServerStateChangeCommand stateChangeCommand = (ServerStateChangeCommand) jedisCommand; +// if (stateChangeCommand.getNewState() != ServerState.RUNNING) +// return; +// MinecraftServer server = stateChangeCommand.getServer(); +// for (Player player : Bukkit.getOnlinePlayers()) { +// if (!server.getName().equals(player.getName() + "-1")) +// continue; +// traveller.sendPlayer(player, server); +// } +// } +// }); + registerCommand(new KingdomCommand(this)); + } + + public boolean host(Player player) { + if (MAX_KINGDOMS <= 0) { + player.sendMessage(Style.error("Kingdom", "§cKingdoms are currently disabled!")); + return false; + } + if (serverGroupRepository.getCached().stream().filter(ServerGroup::isKingdom).count() >= MAX_KINGDOMS) { + player.sendMessage(Style.error("Kingdom", "§cYou cannot create a Kingdom at this time!")); + return false; + } + if (serverGroupRepository.lookup(serverGroup -> serverGroup.getName().equalsIgnoreCase(player.getName())).isPresent()) { + player.sendMessage(Style.error("Kingdom", "§cYou already have a Kingdom created!")); + return false; + } + Optional optionalAccount = AccountManager.fromCache(player.getUniqueId()); + if (!optionalAccount.isPresent()) { + player.sendMessage(Style.error("Kingdom", "§cThere was an error whilst creating your Kingdom!")); + return false; + } + int ram = 1024; + if (optionalAccount.get().hasRank(Rank.DEVELOPER)) + ram = 2048; + ServerGroup currentGroup = MGZPlugin.getMinecraftServer().getGroup(); + ServerGroup serverGroup = new ServerGroup( + player.getName(), + ram, + currentGroup.getServerJar(), + "Arcade.zip", + "McGamerCore-arcade-v1.0-SNAPSHOT.jar", + "HUB/Hub_Normal_2021.zip", + currentGroup.getStartupScript(), + currentGroup.getPrivateAddress(), + player.getUniqueId(), + DEFAULT_GAME.name(), + Integer.MAX_VALUE, + 50, + 1, + 1, + true, + false + ); + serverGroupRepository.post(serverGroup); + return true; + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/kingdom/command/KingdomCommand.java b/core/src/main/java/zone/themcgamer/core/kingdom/command/KingdomCommand.java new file mode 100644 index 0000000..d8a1618 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/kingdom/command/KingdomCommand.java @@ -0,0 +1,27 @@ +package zone.themcgamer.core.kingdom.command; + +import com.cryptomorin.xseries.XSound; +import lombok.AllArgsConstructor; +import org.bukkit.entity.Player; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.core.kingdom.KingdomManager; +import zone.themcgamer.data.Rank; + +/** + * @author Braydon + */ +@AllArgsConstructor +public class KingdomCommand { + private final KingdomManager kingdomManager; + + @Command(name = "kingdom", description = "Host a kingdom", ranks = { Rank.JR_DEVELOPER }, playersOnly = true) + public void onCommand(CommandProvider command) { + Player player = command.getPlayer(); + if (kingdomManager.host(player)) { + player.playSound(player.getEyeLocation(), XSound.ENTITY_PLAYER_LEVELUP.parseSound(), 0.9f, 1f); + player.sendMessage(Style.main("Kingdom", "§aSuccess! §7Your §6Kingdom §7is being setup, you will be sent to it shortly!")); + } + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/module/Module.java b/core/src/main/java/zone/themcgamer/core/module/Module.java new file mode 100644 index 0000000..ac581e6 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/module/Module.java @@ -0,0 +1,67 @@ +package zone.themcgamer.core.module; + +import lombok.Getter; +import org.bukkit.Bukkit; +import org.bukkit.event.Listener; +import org.bukkit.plugin.java.JavaPlugin; +import zone.themcgamer.core.command.CommandManager; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author Braydon + * @implNote The purpose of the class is to easily organize managers. + * Each module has a {@link #onEnable()} and {@link #onDisable()} + * method like a {@link JavaPlugin}. + */ +@Getter +public abstract class Module implements Listener { + @Getter private static final Map, Module> modules = new HashMap<>(); + + private final ModuleInfo info; + private final JavaPlugin plugin; + + public Module(JavaPlugin plugin) { + if (!getClass().isAnnotationPresent(ModuleInfo.class)) + throw new RuntimeException("Cannot initialize module \"" + getClass().getName() + "\" as the @ModuleInfo annotation is missing!"); + info = getClass().getAnnotation(ModuleInfo.class); + this.plugin = plugin; + log("Loading..."); + long started = System.currentTimeMillis(); + onEnable(); + Bukkit.getPluginManager().registerEvents(this, plugin); + log("Loaded in " + (System.currentTimeMillis() - started) + "ms"); + modules.put(getClass(), this); + } + + public void onEnable() {} // Called when the module is enabled + public void onDisable() {} // Called when the module is disabled + + public void registerCommand(Object command) { + CommandManager commandManager = getModule(CommandManager.class); + if (commandManager == null) + throw new NullPointerException("commandManager is null"); + commandManager.registerCommand(plugin, command); + } + + /** + * Log a message to the terminal for this module + * @param message the message to log + */ + public void log(String message) { + System.out.println(info.name() + " » " + message); + } + + /** + * Get the module by the provided class + * @param clazz the class of the module to get + * @return the module + */ + public static T getModule(Class clazz) { + Module module = modules.get(clazz); + if (module == null) + return null; + return clazz.cast(module); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/module/ModuleInfo.java b/core/src/main/java/zone/themcgamer/core/module/ModuleInfo.java new file mode 100644 index 0000000..8a746e5 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/module/ModuleInfo.java @@ -0,0 +1,16 @@ +package zone.themcgamer.core.module; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Braydon + * @implNote The purpose of this class is to provide information for a {@link Module} + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface ModuleInfo { + String name(); +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/nametag/Nametag.java b/core/src/main/java/zone/themcgamer/core/nametag/Nametag.java new file mode 100644 index 0000000..e35f261 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/nametag/Nametag.java @@ -0,0 +1,15 @@ +package zone.themcgamer.core.nametag; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +/** + * @author Braydon (credits: https://github.com/sgtcaze/NametagEdit) + * @implNote This object reprents a nametag for a player + */ +@AllArgsConstructor @Getter @ToString +public class Nametag { + private final String prefix, suffix; + private final int priority; +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/nametag/NametagHandler.java b/core/src/main/java/zone/themcgamer/core/nametag/NametagHandler.java new file mode 100644 index 0000000..304398b --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/nametag/NametagHandler.java @@ -0,0 +1,95 @@ +package zone.themcgamer.core.nametag; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.plugin.java.JavaPlugin; +import zone.themcgamer.core.common.ServerUtils; +import zone.themcgamer.core.common.ServerVersion; +import zone.themcgamer.core.common.Style; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * @author Braydon (credits: https://github.com/sgtcaze/NametagEdit) + */ +public class NametagHandler implements Listener { + public static final boolean DISABLE_PUSH = false; + + private final JavaPlugin plugin; + private final NametagManager nametagManager; + private final Map nametags = new HashMap<>(); + + public NametagHandler(JavaPlugin plugin, NametagManager nametagManager) { + this.plugin = plugin; + this.nametagManager = nametagManager; + Bukkit.getScheduler().runTaskTimer(plugin, this::applyTags, 0L, 20L); + } + + @EventHandler(priority = EventPriority.HIGHEST) + private void onJoin(PlayerJoinEvent event) { + nametagManager.sendTeams(event.getPlayer()); + } + + @EventHandler + private void onQuit(PlayerQuitEvent event) { + nametagManager.reset(event.getPlayer().getName()); + } + + private void applyTags() { + if (!Bukkit.isPrimaryThread()) { + Bukkit.getScheduler().runTask(plugin, this::applyTags); + return; + } + for (Player online : ServerUtils.getLoadedPlayers()) + applyTagToPlayer(online); + } + + private void applyTagToPlayer(Player player) { + // If on the primary thread, run async + if (Bukkit.isPrimaryThread()) { + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> applyTagToPlayer(player)); + return; + } + Nametag nametag = nametags.get(player.getUniqueId()); + if (nametag == null) + return; + Bukkit.getScheduler().runTask(plugin, () -> { + nametagManager.setNametag( + player, + formatWithPlaceholders(player, nametag.getPrefix(), true), + formatWithPlaceholders(player, nametag.getSuffix(), true), + nametag.getPriority() + ); + }); + } + + private String formatWithPlaceholders(Player player, String input, boolean limitChars) { + if (input == null) + return ""; + if (player == null) + return input; + String colored = Style.color(input); + String s = limitChars && colored.length() > 128 ? colored.substring(0, 128) : colored; + switch (ServerVersion.getVersion()) { + case v1_13_R1: + case v1_14_R1: + case v1_14_R2: + case v1_15_R1: + case v1_16_R1: + case v1_16_R2: + case v1_16_R3: { + return s; + } + default: { + return limitChars && colored.length() > 16 ? colored.substring(0, 16) : colored; + } + } + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/nametag/NametagManager.java b/core/src/main/java/zone/themcgamer/core/nametag/NametagManager.java new file mode 100644 index 0000000..5d037b7 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/nametag/NametagManager.java @@ -0,0 +1,125 @@ +package zone.themcgamer.core.nametag; + +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.entity.Player; +import org.bukkit.plugin.java.JavaPlugin; +import zone.themcgamer.core.module.Module; +import zone.themcgamer.core.module.ModuleInfo; +import zone.themcgamer.core.nametag.protocol.PacketWrapper; +import zone.themcgamer.core.nametag.team.FakeTeam; + +import java.util.*; + +/** + * @author Braydon (credits: https://github.com/sgtcaze/NametagEdit) + */ +@ModuleInfo(name = "Nametag Manager") +public class NametagManager extends Module { + private static final HashMap TEAMS = new HashMap<>(); + private static final HashMap CACHED_FAKE_TEAMS = new HashMap<>(); + + public NametagManager(JavaPlugin plugin) { + super(plugin); + Bukkit.getPluginManager().registerEvents(new NametagHandler(plugin, this), plugin); + } + + public void setNametag(Player player, String prefix, String suffix, int priority) { + if (prefix == null) + prefix = "§r"; + if (suffix == null) + suffix = "§r"; + FakeTeam previous = getFakeTeam(player); + if (previous != null && previous.isSimilar(prefix, suffix)) + return; + reset(player); + FakeTeam team = getFakeTeam(prefix, suffix); + if (team != null) { + team.addMember(player); + } else { + team = new FakeTeam(prefix, suffix, priority); + team.addMember(player); + TEAMS.put(team.getName(), team); + addTeamPackets(team); + } + addPlayerToTeamPackets(team, player.getName()); + cache(player.getName(), team); + } + + protected void sendTeams(Player player) { + for (FakeTeam fakeTeam : TEAMS.values()) + new PacketWrapper(fakeTeam.getName(), fakeTeam.getPrefix(), fakeTeam.getSuffix(), 0, fakeTeam.getMembers()).send(player); + } + + protected void reset(Player player) { + reset(player.getName()); + } + + protected void reset(String playerName) { + reset(playerName, CACHED_FAKE_TEAMS.remove(playerName)); + } + + public void cleanup() { + for (FakeTeam fakeTeam : TEAMS.values()) { + removePlayerFromTeamPackets(fakeTeam, fakeTeam.getMembers()); + removeTeamPackets(fakeTeam); + } + TEAMS.clear(); + CACHED_FAKE_TEAMS.clear(); + } + + private void reset(String player, FakeTeam fakeTeam) { + if (fakeTeam != null && fakeTeam.getMembers().remove(player)) { + boolean delete; + Player removing = Bukkit.getPlayerExact(player); + if (removing != null) { + delete = removePlayerFromTeamPackets(fakeTeam, removing.getName()); + } else { + OfflinePlayer toRemoveOffline = Bukkit.getOfflinePlayer(player); + delete = removePlayerFromTeamPackets(fakeTeam, toRemoveOffline.getName()); + } + if (delete) { + removeTeamPackets(fakeTeam); + TEAMS.remove(fakeTeam.getName()); + } + } + } + + private void cache(String playerName, FakeTeam fakeTeam) { + CACHED_FAKE_TEAMS.put(playerName, fakeTeam); + } + + private FakeTeam getFakeTeam(Player player) { + return getFakeTeam(player.getName()); + } + + private FakeTeam getFakeTeam(String playerName) { + return CACHED_FAKE_TEAMS.get(playerName); + } + + private FakeTeam getFakeTeam(String prefix, String suffix) { + return TEAMS.values().stream().filter(fakeTeam -> fakeTeam.isSimilar(prefix, suffix)).findFirst().orElse(null); + } + + private void addTeamPackets(FakeTeam fakeTeam) { + new PacketWrapper(fakeTeam.getName(), fakeTeam.getPrefix(), fakeTeam.getSuffix(), 0, fakeTeam.getMembers()).send(); + } + + private void addPlayerToTeamPackets(FakeTeam fakeTeam, String player) { + new PacketWrapper(fakeTeam.getName(), 3, Collections.singletonList(player)).send(); + } + + private boolean removePlayerFromTeamPackets(FakeTeam fakeTeam, String... players) { + return removePlayerFromTeamPackets(fakeTeam, Arrays.asList(players)); + } + + private boolean removePlayerFromTeamPackets(FakeTeam fakeTeam, List players) { + new PacketWrapper(fakeTeam.getName(), 4, players).send(); + fakeTeam.getMembers().removeAll(players); + return fakeTeam.getMembers().isEmpty(); + } + + private void removeTeamPackets(FakeTeam fakeTeam) { + new PacketWrapper(fakeTeam.getName(), fakeTeam.getPrefix(), fakeTeam.getSuffix(), 1, new ArrayList<>()).send(); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/nametag/protocol/PacketAccessor.java b/core/src/main/java/zone/themcgamer/core/nametag/protocol/PacketAccessor.java new file mode 100644 index 0000000..c6c8f6f --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/nametag/protocol/PacketAccessor.java @@ -0,0 +1,123 @@ +package zone.themcgamer.core.nametag.protocol; + +import net.minecraft.server.v1_12_R1.PacketPlayOutScoreboardTeam; +import org.bukkit.entity.Player; +import zone.themcgamer.core.common.ServerVersion; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collection; + +/** + * @author Braydon (credits: https://github.com/sgtcaze/NametagEdit) + */ +public class PacketAccessor { + public static Field MEMBERS; + public static Field PREFIX; + public static Field SUFFIX; + public static Field TEAM_NAME; + public static Field PARAM_INT; + public static Field PACK_OPTION; + public static Field DISPLAY_NAME; + public static Field TEAM_COLOR; + public static Field PUSH; + public static Field VISIBILITY; + + private static Method getHandle; + private static Method sendPacket; + private static Field playerConnection; + + private static Class packetClass; + + static { + try { + String version = ServerVersion.NMS_VERSION; + Class typeCraftPlayer = Class.forName("org.bukkit.craftbukkit." + version + ".entity.CraftPlayer"); + getHandle = typeCraftPlayer.getMethod("getHandle"); + + packetClass = Class.forName("net.minecraft.server." + version + ".PacketPlayOutScoreboardTeam"); + Class typeNMSPlayer = Class.forName("net.minecraft.server." + version + ".EntityPlayer"); + Class typePlayerConnection = Class.forName("net.minecraft.server." + version + ".PlayerConnection"); + playerConnection = typeNMSPlayer.getField("playerConnection"); + sendPacket = typePlayerConnection.getMethod("sendPacket", Class.forName("net.minecraft.server." + version + ".Packet")); + + PacketData currentVersion = Arrays.stream(PacketData.values()) + .filter(packetData -> version.contains(packetData.name())) + .findFirst().orElse(null); + if (currentVersion != null) { + MEMBERS = getField(currentVersion.getMembers()); + PREFIX = getField(currentVersion.getPrefix()); + SUFFIX = getField(currentVersion.getSuffix()); + TEAM_NAME = getField(currentVersion.getTeamName()); + PARAM_INT = getField(currentVersion.getParamInt()); + PACK_OPTION = getField(currentVersion.getPackOption()); + DISPLAY_NAME = getField(currentVersion.getDisplayName()); + + // If the server is running a native version, we wanna support team colors + if (ServerVersion.getVersion().isNativeVersion()) + TEAM_COLOR = getField(currentVersion.getColor()); + + // If the version is a pushed version + if (Integer.parseInt(version.split("_")[1]) >= 9) + PUSH = getField(currentVersion.getPush()); + + // If the version is a visibility version + if (Integer.parseInt(version.split("_")[1]) >= 8) + VISIBILITY = getField(currentVersion.getVisibility()); + } + } catch (Exception ex) { + ex.printStackTrace(); + } + } + + /** + * Create a new instance of {@link PacketPlayOutScoreboardTeam} + * @return the new instance + */ + public static Object createPacket() { + try { + return packetClass.newInstance(); + } catch (Exception ex) { + ex.printStackTrace(); + return null; + } + } + + /** + * Send the given packet to the {@link Collection} + * @param players the players to send the packet to + * @param packet the packet to send + */ + public static void sendPacket(Collection players, Object packet) { + for (Player player : players) + sendPacket(player, packet); + } + + /** + * Send a packet to the given {@link Player} + * @param player the player to send the packet to + * @param packet the packet to send + */ + public static void sendPacket(Player player, Object packet) { + try { + Object nmsPlayer = getHandle.invoke(player); + Object connection = playerConnection.get(nmsPlayer); + sendPacket.invoke(connection, packet); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + + /** + * Get a field in the packet by the given name + * @param name the name of the field + * @return the field + * @throws Exception the exception + */ + private static Field getField(String name) throws Exception { + Field field = packetClass.getDeclaredField(name); + field.setAccessible(true); + return field; + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/nametag/protocol/PacketData.java b/core/src/main/java/zone/themcgamer/core/nametag/protocol/PacketData.java new file mode 100644 index 0000000..ded1fad --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/nametag/protocol/PacketData.java @@ -0,0 +1,26 @@ +package zone.themcgamer.core.nametag.protocol; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import net.minecraft.server.v1_12_R1.PacketPlayOutScoreboardTeam; + +/** + * @author Braydon (credits: https://github.com/sgtcaze/NametagEdit) + * @implNote This class holds the field names for the {@link PacketPlayOutScoreboardTeam} packet for each + * Minecraft version + */ +@AllArgsConstructor @Getter +public enum PacketData { + v1_7("e", "c", "d", "a", "f", "g", "b", "NA", "NA", "NA"), + v1_8("g", "c", "d", "a", "h", "i", "b", "NA", "NA", "e"), + v1_9("h", "c", "d", "a", "i", "j", "b", "NA", "f", "e"), + v1_10("h", "c", "d", "a", "i", "j", "b", "NA", "f", "e"), + v1_11("h", "c", "d", "a", "i", "j", "b", "NA", "f", "e"), + v1_12("h", "c", "d", "a", "i", "j", "b", "NA", "f", "e"), + v1_13("h", "c", "d", "a", "i", "j", "b", "g", "f", "e"), + v1_14("h", "c", "d", "a", "i", "j", "b", "g", "f", "e"), + v1_15("h", "c", "d", "a", "i", "j", "b", "g", "f", "e"), + v1_16("h", "c", "d", "a", "i", "j", "b", "g", "f", "e"); + + private final String members, prefix, suffix, teamName, paramInt, packOption, displayName, color, push, visibility; +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/nametag/protocol/PacketWrapper.java b/core/src/main/java/zone/themcgamer/core/nametag/protocol/PacketWrapper.java new file mode 100644 index 0000000..809c222 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/nametag/protocol/PacketWrapper.java @@ -0,0 +1,119 @@ +package zone.themcgamer.core.nametag.protocol; + +import org.bukkit.ChatColor; +import org.bukkit.entity.Player; +import zone.themcgamer.core.common.ServerUtils; +import zone.themcgamer.core.common.ServerVersion; +import zone.themcgamer.core.nametag.NametagHandler; + +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * @author Braydon (credits: https://github.com/sgtcaze/NametagEdit) + */ +@SuppressWarnings({"unchecked", "rawtypes"}) +public class PacketWrapper { + private static Constructor CHAT_COMPONENT_CONSTRUCTOR; + private static Class CHAT_FORMAT_TYPE; + + static { + if (ServerVersion.getVersion().isNativeVersion()) { + String version = ServerVersion.NMS_VERSION; + try { + Class typeChatComponentText = Class.forName("net.minecraft.server." + version + ".ChatComponentText"); + CHAT_COMPONENT_CONSTRUCTOR = typeChatComponentText.getConstructor(String.class); + CHAT_FORMAT_TYPE = (Class>) Class.forName("net.minecraft.server." + version + ".EnumChatFormat"); + } catch (ClassNotFoundException | NoSuchMethodException ex) { + ex.printStackTrace(); + } + } + } + + private final Object packet = PacketAccessor.createPacket(); + + public PacketWrapper(String name, int param, List members) { + if (param != 3 && param != 4) + throw new IllegalArgumentException("Method must be join or leave for player constructor"); + setupDefaults(name, param); + setupMembers(members); + } + + public PacketWrapper(String name, String prefix, String suffix, int param, Collection players) { + setupDefaults(name, param); + + if (param == 0 || param == 2) { + try { + if (param == 0) + ((Collection) PacketAccessor.MEMBERS.get(packet)).addAll(players); + if (ServerVersion.getVersion().isLegacy()) { + PacketAccessor.PREFIX.set(packet, prefix); + PacketAccessor.SUFFIX.set(packet, suffix); + PacketAccessor.DISPLAY_NAME.set(packet, name); + } else { + String color = ChatColor.getLastColors(prefix); + String colorCode = null; + + if (!color.isEmpty()) { + colorCode = color.substring(color.length() - 1); + String chatColor = ChatColor.getByChar(colorCode).name(); + if (chatColor.equalsIgnoreCase("MAGIC")) + chatColor = "OBFUSCATED"; + Enum colorEnum = Enum.valueOf(CHAT_FORMAT_TYPE, chatColor); + PacketAccessor.TEAM_COLOR.set(packet, colorEnum); + } + PacketAccessor.PREFIX.set(packet, CHAT_COMPONENT_CONSTRUCTOR.newInstance(prefix)); + + if (colorCode != null) + suffix = ChatColor.getByChar(colorCode) + suffix; + PacketAccessor.SUFFIX.set(packet, CHAT_COMPONENT_CONSTRUCTOR.newInstance(suffix)); + + PacketAccessor.DISPLAY_NAME.set(packet, CHAT_COMPONENT_CONSTRUCTOR.newInstance(name)); + } + PacketAccessor.PACK_OPTION.set(packet, 1); + + if (PacketAccessor.VISIBILITY != null) + PacketAccessor.VISIBILITY.set(packet, "always"); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + } + + /** + * Send the wrapped packet to the loaded players in the server + */ + public void send() { + PacketAccessor.sendPacket(ServerUtils.getLoadedPlayers(), packet); + } + + /** + * Send the wrapped packet to the given {@link Player} + * @param player the player to send the wrapped packet to + */ + public void send(Player player) { + PacketAccessor.sendPacket(player, packet); + } + + private void setupDefaults(String name, int param) { + try { + PacketAccessor.TEAM_NAME.set(packet, name); + PacketAccessor.PARAM_INT.set(packet, param); + if (NametagHandler.DISABLE_PUSH && PacketAccessor.PUSH != null) + PacketAccessor.PUSH.set(packet, "never"); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + + private void setupMembers(Collection players) { + try { + players = players == null || players.isEmpty() ? new ArrayList<>() : players; + ((Collection) PacketAccessor.MEMBERS.get(packet)).addAll(players); + } catch (Exception ex) { + ex.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/nametag/team/FakeTeam.java b/core/src/main/java/zone/themcgamer/core/nametag/team/FakeTeam.java new file mode 100644 index 0000000..e3cf10c --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/nametag/team/FakeTeam.java @@ -0,0 +1,93 @@ +package zone.themcgamer.core.nametag.team; + +import lombok.Data; +import org.bukkit.entity.Player; +import zone.themcgamer.core.common.ServerVersion; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Braydon (credits: https://github.com/sgtcaze/NametagEdit) + */ +@Data +public class FakeTeam { + private static final String UNIQUE_ID; + private static int ID; + + static { + // Generating a unique ID and applying it to the field so it can be used in team names + String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < 5; i++) + builder.append(chars.charAt((int) (Math.random() * chars.length()))); + UNIQUE_ID = builder.toString(); + } + + private final String name, prefix, suffix; + private final List members = new ArrayList<>(); + + public FakeTeam(String prefix, String suffix, int priority) { + // Priority + String letter; + if (priority < 0) + letter = "Z"; + else { + char letterCharacter = (char) ((priority / 5) + 65); + int repeat = priority % 5 + 1; + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < repeat; i++) + builder.append(letterCharacter); + letter = builder.toString(); + } + String name; + name = UNIQUE_ID + "_" + letter + ++ID; + + switch (ServerVersion.getVersion()) { + case v1_13_R1: + case v1_14_R1: + case v1_14_R2: + case v1_15_R1: + case v1_16_R1: + case v1_16_R2: + case v1_16_R3: { + name = name.length() > 128 ? name.substring(0, 128) : name; + break; + } + default: { + name = name.length() > 16 ? name.substring(0, 16) : name; + } + } + + this.name = name; + this.prefix = prefix; + this.suffix = suffix; + } + + /** + * Add the given {@link Player} to the team + * @param player the player to add + */ + public void addMember(Player player) { + addMember(player.getName()); + } + + /** + * Add the given player name to the team + * @param playerName the player name to add + */ + public void addMember(String playerName) { + if (!members.contains(playerName)) + members.add(playerName); + } + + /** + * Check if the given prefix and suffix is similar to the team prefix and suffix + * @param prefix the prefix + * @param suffix the suffix + * @return if the given prefix and suffix are similar + */ + public boolean isSimilar(String prefix, String suffix) { + return this.prefix.equals(prefix) && this.suffix.equals(suffix); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/plugin/MGZPlugin.java b/core/src/main/java/zone/themcgamer/core/plugin/MGZPlugin.java new file mode 100644 index 0000000..f9ad880 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/plugin/MGZPlugin.java @@ -0,0 +1,215 @@ +package zone.themcgamer.core.plugin; + +import lombok.Getter; +import lombok.SneakyThrows; +import org.bukkit.Bukkit; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.java.JavaPlugin; +import zone.themcgamer.core.account.AccountManager; +import zone.themcgamer.core.badSportSystem.BadSportSystem; +import zone.themcgamer.core.command.CommandManager; +import zone.themcgamer.core.common.ServerUtils; +import zone.themcgamer.core.common.menu.MenuManager; +import zone.themcgamer.core.common.scheduler.Scheduler; +import zone.themcgamer.core.cooldown.CooldownHandler; +import zone.themcgamer.core.module.Module; +import zone.themcgamer.core.nametag.NametagManager; +import zone.themcgamer.core.plugin.command.BuildDataCommand; +import zone.themcgamer.core.plugin.command.PluginsCommand; +import zone.themcgamer.core.server.ServerManager; +import zone.themcgamer.core.task.TaskManager; +import zone.themcgamer.core.traveller.ServerTraveller; +import zone.themcgamer.core.update.ServerUpdater; +import zone.themcgamer.data.jedis.JedisController; +import zone.themcgamer.data.jedis.data.server.MinecraftServer; +import zone.themcgamer.data.jedis.data.server.ServerState; +import zone.themcgamer.data.jedis.repository.RedisRepository; +import zone.themcgamer.data.jedis.repository.impl.MinecraftServerRepository; +import zone.themcgamer.data.jedis.repository.impl.ServerGroupRepository; +import zone.themcgamer.data.mysql.MySQLController; + +import java.io.File; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * @author Braydon + */ +public abstract class MGZPlugin extends JavaPlugin { + @Getter private static MinecraftServer minecraftServer; + + protected JedisController jedisController; + protected MySQLController mySQLController; + protected CommandManager commandManager; + protected ServerTraveller traveller; + protected BadSportSystem badSportSystem; + protected NametagManager nametagManager; + + @SneakyThrows + @Override + public void onEnable() { + // Connect to Redis and setup the controller + getLogger().info("Connecting to Redis..."); + jedisController = new JedisController().start(); + + getLogger().info("Setting up Minecraft server..."); + Optional optionalMinecraftServerRepository = RedisRepository.getRepository(MinecraftServerRepository.class); + if (!optionalMinecraftServerRepository.isPresent()) { + getLogger().severe("Cannot find Minecraft server repository, stopping..."); + getServer().shutdown(); + return; + } + MinecraftServerRepository minecraftServerRepository = optionalMinecraftServerRepository.get(); + minecraftServerRepository.addUpdateListener(servers -> { + if (minecraftServer != null || servers.isEmpty()) + return; + Optional optionalMinecraftServer = servers.stream() + .filter(server -> server.getNode() != null) + .filter(server -> { + try { + return server.getNode().getName().equals(InetAddress.getLocalHost().getHostName()) && server.getPort() == Bukkit.getPort(); + } catch (UnknownHostException ex) { + ex.printStackTrace(); + } + return false; + }).findFirst(); + // If there is no MinecraftServer found with this server ip and port or the server found is not + // in a starting state, we wanna attempt to load the MinecraftServer information from a local file + if (!optionalMinecraftServer.isPresent() || (optionalMinecraftServer.get().getState() != ServerState.STARTING)) { + File detailsFile = new File(getDataFolder(), "details.yml"); + if (detailsFile.exists()) { // If the details file exists, try and load the MinecraftServer from it + try { + FileConfiguration configuration = YamlConfiguration.loadConfiguration(detailsFile); + + String name = configuration.getString("name"); + String groupName = configuration.getString("group"); + + ServerGroupRepository serverGroupRepository = RedisRepository.getRepository(ServerGroupRepository.class).orElse(null); + if (serverGroupRepository == null) + throw new NullPointerException(); + long now = System.currentTimeMillis(); + optionalMinecraftServer = Optional.of(new MinecraftServer( + name, + Integer.parseInt(name.split("-")[1]), + name, + null, + serverGroupRepository.lookup(groupName).orElse(null), + "168.119.4.237", + getServer().getPort(), + 0, + 0, + ServerState.STARTING, + now, + 0, + 0, + 20.0D, + null, + "", + "", + now, + now + )); + } catch (Exception ex) { + ex.printStackTrace(); + getServer().shutdown(); + return; + } + } else { // If the details file doesn't exist and the server wasn't found in Redis, display an error and stop the server + getLogger().severe("Cannot find Minecraft server, stopping...:"); + if (!servers.isEmpty()) { + getLogger().info("Minecraft Servers:"); + for (MinecraftServer server : servers) + getLogger().info(" " + server.toString()); + } + getServer().shutdown(); + return; + } + } + minecraftServer = optionalMinecraftServer.get(); + minecraftServer.setState(ServerState.RUNNING); // Set the MinecraftServer to the running state + + // Updating the MinecraftServer in Redis + Bukkit.getScheduler().scheduleSyncRepeatingTask(this, () -> { + Runtime runtime = Runtime.getRuntime(); + + minecraftServer.setUsedRam((int) formatMemory(runtime.totalMemory() - runtime.freeMemory())); + minecraftServer.setMaxRam((int) formatMemory(runtime.maxMemory())); + minecraftServer.setOnline(Bukkit.getOnlinePlayers().size()); + minecraftServer.setMaxPlayers(Bukkit.getMaxPlayers()); + minecraftServer.setTps(ServerUtils.getTps()); + minecraftServer.setLastHeartbeat(System.currentTimeMillis()); + + minecraftServerRepository.post(minecraftServer); + }, 0L, 2L * 20L); + + // Starting up MySQL + getLogger().info("Connecting to MySQL..."); + mySQLController = new MySQLController(false); + + // Loading utilities + getLogger().info("Loading utilities..."); + new Scheduler(this); + new MenuManager(this); + + // Loading essential modules that will always be enabled + getLogger().info("Loading essential modules..."); + + commandManager = new CommandManager(this); + commandManager.registerCommand(this, new BuildDataCommand()); + commandManager.registerCommand(this, new PluginsCommand()); + + new CooldownHandler(this); + nametagManager = new NametagManager(this); + + AccountManager accountManager = new AccountManager(this, mySQLController, nametagManager); + traveller = new ServerTraveller(this); + new ServerUpdater(this, traveller); + new ServerManager(this, traveller); + + badSportSystem = new BadSportSystem(this, mySQLController, accountManager); + AccountManager.addMiniAccount(new TaskManager(this)); + + // Running the @Startup methods for the plugin + getLogger().info("Running @Startup methods..."); + List methods = Arrays.stream(getClass().getMethods()) + .filter(method -> method.isAnnotationPresent(Startup.class)) + .sorted(Comparator.comparingInt(a -> a.getAnnotation(Startup.class).priority())) + .collect(Collectors.toList()); + for (Method method : methods) { + try { + method.invoke(this); + } catch (IllegalAccessException | InvocationTargetException ex) { + ex.printStackTrace(); + } + } + }); + } + + @Override + public void onDisable() { + minecraftServer.setState(ServerState.STOPPING); // Set the MinecraftServer to the stopping state + commandManager.cleanup(); // Cleanup the command manager + for (Module module : Module.getModules().values()) // Disable all modules + module.onDisable(); + nametagManager.cleanup(); + } + + /** + * Format the given memory into megabytes + * @param memory the memory to format + * @return the formatted memory + */ + private double formatMemory(double memory) { + if (memory < 1024) + return memory; + return Math.round(memory / 1024 / 1024); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/plugin/Startup.java b/core/src/main/java/zone/themcgamer/core/plugin/Startup.java new file mode 100644 index 0000000..1c9d53a --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/plugin/Startup.java @@ -0,0 +1,15 @@ +package zone.themcgamer.core.plugin; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Braydon + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Startup { + int priority() default 1; +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/plugin/command/BuildDataCommand.java b/core/src/main/java/zone/themcgamer/core/plugin/command/BuildDataCommand.java new file mode 100644 index 0000000..5a85ff5 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/plugin/command/BuildDataCommand.java @@ -0,0 +1,24 @@ +package zone.themcgamer.core.plugin.command; + +import org.bukkit.command.CommandSender; +import zone.themcgamer.common.BuildData; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.data.Rank; + +/** + * @author Braydon + */ +public class BuildDataCommand { + @Command(name = "builddata", ranks = { Rank.JR_DEVELOPER }, description = "Show the build information") + public void onCommand(CommandProvider command) { + CommandSender sender = command.getSender(); + BuildData build = BuildData.getBuild(); + sender.sendMessage(Style.main("Build Data", "Build Information:")); + sender.sendMessage(" §8- §7Branch §f" + build.getBranch() + "/" + build.getModule()); + sender.sendMessage(" §8- §7Username §f" + build.getUsername() + "@" + build.getHost()); + sender.sendMessage(" §8- §7Version §f" + build.getVersion()); + sender.sendMessage(" §8- §7Time §f" + build.getTime()); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/plugin/command/PluginsCommand.java b/core/src/main/java/zone/themcgamer/core/plugin/command/PluginsCommand.java new file mode 100644 index 0000000..7b358cc --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/plugin/command/PluginsCommand.java @@ -0,0 +1,22 @@ +package zone.themcgamer.core.plugin.command; + +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.data.Rank; + +import java.util.ArrayList; +import java.util.List; + +public class PluginsCommand { + @Command(name = "plugins", aliases = { "pl" }, ranks = { Rank.JR_DEVELOPER }, description = "Get a list of plugins") + public void onCommand(CommandProvider command) { + List pluginNames = new ArrayList<>(); + for (Plugin plugin : Bukkit.getPluginManager().getPlugins()) + pluginNames.add((plugin.isEnabled() ? "§a" + plugin.getName() : "§c" + plugin.getName())); + command.getSender().sendMessage(Style.main("§9§lPlugins &7(" + pluginNames.size() + ")", + String.join("§7, §f", pluginNames))); + } +} diff --git a/core/src/main/java/zone/themcgamer/core/server/ServerManager.java b/core/src/main/java/zone/themcgamer/core/server/ServerManager.java new file mode 100644 index 0000000..5b04383 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/server/ServerManager.java @@ -0,0 +1,66 @@ +package zone.themcgamer.core.server; + +import org.bukkit.Bukkit; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.player.AsyncPlayerPreLoginEvent; +import org.bukkit.plugin.java.JavaPlugin; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.core.module.Module; +import zone.themcgamer.core.module.ModuleInfo; +import zone.themcgamer.core.plugin.MGZPlugin; +import zone.themcgamer.core.server.command.*; +import zone.themcgamer.core.traveller.ServerTraveller; +import zone.themcgamer.data.jedis.command.JedisCommandHandler; +import zone.themcgamer.data.jedis.command.impl.ServerRestartCommand; +import zone.themcgamer.data.jedis.data.server.MinecraftServer; +import zone.themcgamer.data.jedis.data.server.ServerState; +import zone.themcgamer.data.jedis.repository.RedisRepository; +import zone.themcgamer.data.jedis.repository.impl.MinecraftServerRepository; +import zone.themcgamer.data.jedis.repository.impl.ServerGroupRepository; + +/** + * @author Braydon + */ +@ModuleInfo(name = "Server Manager") +public class ServerManager extends Module { + public ServerManager(JavaPlugin plugin, ServerTraveller traveller) { + super(plugin); + ServerGroupRepository serverGroupRepository = RedisRepository.getRepository(ServerGroupRepository.class).orElse(null); + MinecraftServerRepository minecraftServerRepository = RedisRepository.getRepository(MinecraftServerRepository.class).orElse(null); + registerCommand(new MonitorCommand()); + registerCommand(new ServerCommand(traveller, minecraftServerRepository)); + registerCommand(new HubCommand(traveller)); + registerCommand(new RestartCommand(this, serverGroupRepository, minecraftServerRepository)); + registerCommand(new StopCommand(this)); + + // Handle server restarting + JedisCommandHandler.getInstance().addListener(jedisCommand -> { + if (jedisCommand instanceof ServerRestartCommand) { + MinecraftServer minecraftServer = MGZPlugin.getMinecraftServer(); + if (!((ServerRestartCommand) jedisCommand).getServerId().equals(minecraftServer.getId())) + return; + try { + traveller.sendAll("Hub", "&6" + minecraftServer.getName() + " &7is restarting"); + } catch (IllegalArgumentException ignored) {} + Bukkit.getScheduler().scheduleSyncDelayedTask(getPlugin(), () -> + minecraftServer.setState(ServerState.RESTARTING), 10L); + Bukkit.getScheduler().scheduleSyncDelayedTask(getPlugin(), Bukkit::shutdown, 40L); + } + }); + } + + @EventHandler(priority = EventPriority.HIGHEST) + private void onJoin(AsyncPlayerPreLoginEvent event) { + if (!MGZPlugin.getMinecraftServer().isRunning()) + event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, Style.color("&cThis server is currently restarting...")); + } + + /** + * Restart the given {@link MinecraftServer} + * @param minecraftServer the server to restart + */ + public void restart(MinecraftServer minecraftServer) { + JedisCommandHandler.getInstance().send(new ServerRestartCommand(minecraftServer.getId())); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/server/command/HubCommand.java b/core/src/main/java/zone/themcgamer/core/server/command/HubCommand.java new file mode 100644 index 0000000..54855ee --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/server/command/HubCommand.java @@ -0,0 +1,26 @@ +package zone.themcgamer.core.server.command; + +import lombok.AllArgsConstructor; +import org.bukkit.entity.Player; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.core.traveller.ServerTraveller; + +/** + * @author Braydon + */ +@AllArgsConstructor +public class HubCommand { + private final ServerTraveller traveller; + + @Command(name = "hub", aliases = { "lobby" }, description = "Join a random hub", playersOnly = true) + public void onCommand(CommandProvider command) { + Player player = command.getPlayer(); + try { + traveller.sendPlayer(player, "Hub"); + } catch (Exception ex) { + player.sendMessage(Style.error("Server", "&7Could not find an available hub to send you to!")); + } + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/server/command/MonitorCommand.java b/core/src/main/java/zone/themcgamer/core/server/command/MonitorCommand.java new file mode 100644 index 0000000..ed962d3 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/server/command/MonitorCommand.java @@ -0,0 +1,16 @@ +package zone.themcgamer.core.server.command; + +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.core.server.menu.ServerMonitorMenu; +import zone.themcgamer.data.Rank; + +/** + * @author Braydon + */ +public class MonitorCommand { + @Command(name = "monitor", description = "Monitor the server", ranks = { Rank.HELPER }, playersOnly = true) + public void onCommand(CommandProvider command) { + new ServerMonitorMenu(command.getPlayer()).open(); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/server/command/RestartCommand.java b/core/src/main/java/zone/themcgamer/core/server/command/RestartCommand.java new file mode 100644 index 0000000..eb584e6 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/server/command/RestartCommand.java @@ -0,0 +1,54 @@ +package zone.themcgamer.core.server.command; + +import lombok.AllArgsConstructor; +import org.bukkit.command.CommandSender; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.core.server.ServerManager; +import zone.themcgamer.data.Rank; +import zone.themcgamer.data.jedis.data.ServerGroup; +import zone.themcgamer.data.jedis.data.server.MinecraftServer; +import zone.themcgamer.data.jedis.repository.impl.MinecraftServerRepository; +import zone.themcgamer.data.jedis.repository.impl.ServerGroupRepository; + +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +/** + * @author Braydon + */ +@AllArgsConstructor +public class RestartCommand { + private final ServerManager serverManager; + private final ServerGroupRepository serverGroupRepository; + private final MinecraftServerRepository minecraftServerRepository; + + @Command(name = "restart", aliases = { "reboot" }, description = "Restart a server or server group", ranks = { Rank.ADMIN }) + public void onCommand(CommandProvider command) { + CommandSender sender = command.getSender(); + String[] args = command.getArgs(); + if (args.length < 1) { + sender.sendMessage(Style.main("Server", "You must provide a server or server group to restart")); + return; + } + Set toRestart = new HashSet<>(); + Optional optionalServerGroup = serverGroupRepository.lookup(args[0]); + optionalServerGroup.ifPresent(serverGroup -> toRestart.addAll(serverGroup.getServers())); + if (toRestart.isEmpty()) { + minecraftServerRepository.lookup(minecraftServer -> minecraftServer.getId().equals(args[0]) + || minecraftServer.getName().equalsIgnoreCase(args[0])) + .ifPresent(toRestart::add); + } + // Static servers can't be restarted via this command as they may not come back online + toRestart.removeIf(minecraftServer -> minecraftServer.getGroup().isStaticGroup()); + if (toRestart.isEmpty()) { + sender.sendMessage(Style.error("Server", "&7Could not find any servers to restart!")); + return; + } + for (MinecraftServer minecraftServer : toRestart) + serverManager.restart(minecraftServer); + sender.sendMessage(Style.main("Server", "Restarting &6" + toRestart.size() + " server" + (toRestart.size() == 1 ? "" : "s"))); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/server/command/ServerCommand.java b/core/src/main/java/zone/themcgamer/core/server/command/ServerCommand.java new file mode 100644 index 0000000..4a7f62b --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/server/command/ServerCommand.java @@ -0,0 +1,61 @@ +package zone.themcgamer.core.server.command; + +import lombok.AllArgsConstructor; +import net.md_5.bungee.api.chat.ComponentBuilder; +import net.md_5.bungee.api.chat.HoverEvent; +import org.bukkit.entity.Player; +import zone.themcgamer.common.MiscUtils; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.core.plugin.MGZPlugin; +import zone.themcgamer.core.traveller.ServerTraveller; +import zone.themcgamer.data.jedis.data.server.MinecraftServer; +import zone.themcgamer.data.jedis.repository.impl.MinecraftServerRepository; + +import java.util.Optional; + +/** + * @author Braydon + * TODO: Make it so you can do /join and join the best server available for that game + */ +@AllArgsConstructor +public class ServerCommand { + private final ServerTraveller traveller; + private final MinecraftServerRepository minecraftServerRepository; + + @Command(name = "server", aliases = { "join", "play" }, description = "Join a server", playersOnly = true) + public void onCommand(CommandProvider command) { + Player player = command.getPlayer(); + String[] args = command.getArgs(); + MinecraftServer currentServer = MGZPlugin.getMinecraftServer(); + if (args.length < 1) { + player.sendMessage(new ComponentBuilder(Style.main("Server", "You're currently on &6" + currentServer.getName())) + .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder(MiscUtils.arrayToString( + Style.color("&7Server Id: &6" + currentServer.getId()), + Style.color("&7Type: &6" + currentServer.getGroup().getName()) + )).create())).create()); + return; + } + Optional optionalServer = minecraftServerRepository + .lookup(minecraftServer -> minecraftServer.getName().equalsIgnoreCase(args[0])); + if (!optionalServer.isPresent()) { + player.sendMessage(Style.error("Server", "&7A server with that name doesn't exist!")); + return; + } + MinecraftServer minecraftServer = optionalServer.get(); + if (minecraftServer.equals(currentServer)) { + player.sendMessage(new ComponentBuilder(Style.main("Server", "You're already connected to &6" + currentServer.getName())) + .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder(MiscUtils.arrayToString( + Style.color("&7Server Id: &6" + currentServer.getId()), + Style.color("&7Type: &6" + currentServer.getGroup().getName()) + )).create())).create()); + return; + } + try { + traveller.sendPlayer(player, minecraftServer); + } catch (Exception ex) { + player.sendMessage(Style.error("Server", "&7Cannot join &6" + minecraftServer.getName() + " &7at this time: &b" + ex.getLocalizedMessage())); + } + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/server/command/StopCommand.java b/core/src/main/java/zone/themcgamer/core/server/command/StopCommand.java new file mode 100644 index 0000000..2acdfc7 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/server/command/StopCommand.java @@ -0,0 +1,20 @@ +package zone.themcgamer.core.server.command; + +import lombok.RequiredArgsConstructor; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.core.plugin.MGZPlugin; +import zone.themcgamer.core.server.ServerManager; +import zone.themcgamer.data.Rank; + +@RequiredArgsConstructor +public class StopCommand { + private final ServerManager serverManager; + + @Command(name = "stop", aliases = { "stopserver" }, description = "Stop this server", ranks = { Rank.ADMIN }) + public void onCommand(CommandProvider command) { + command.getSender().sendMessage(Style.main("Server", "&aSafely stopping the Minecraft server...")); + serverManager.restart(MGZPlugin.getMinecraftServer()); + } +} diff --git a/core/src/main/java/zone/themcgamer/core/server/menu/ServerMonitorMenu.java b/core/src/main/java/zone/themcgamer/core/server/menu/ServerMonitorMenu.java new file mode 100644 index 0000000..8c8c31c --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/server/menu/ServerMonitorMenu.java @@ -0,0 +1,59 @@ +package zone.themcgamer.core.server.menu; + +import com.cryptomorin.xseries.XMaterial; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import zone.themcgamer.common.CpuMonitor; +import zone.themcgamer.common.TimeUtils; +import zone.themcgamer.core.account.Account; +import zone.themcgamer.core.account.AccountManager; +import zone.themcgamer.core.common.ItemBuilder; +import zone.themcgamer.core.common.MiscUtils; +import zone.themcgamer.core.common.ServerUtils; +import zone.themcgamer.core.common.menu.Button; +import zone.themcgamer.core.common.menu.MenuType; +import zone.themcgamer.core.common.menu.UpdatableMenu; +import zone.themcgamer.core.plugin.MGZPlugin; +import zone.themcgamer.data.Rank; +import zone.themcgamer.data.jedis.data.server.MinecraftServer; + +import java.lang.management.ManagementFactory; +import java.util.Optional; + +public class ServerMonitorMenu extends UpdatableMenu { + public ServerMonitorMenu(Player player) { + super(player, "Server Monitor", 1, MenuType.CHEST); + } + + @Override + public void onUpdate() { + Optional optionalAccount = AccountManager.fromCache(player.getUniqueId()); + if (optionalAccount.isEmpty()) + return; + int slot = 0; + if (optionalAccount.get().hasRank(Rank.JR_DEVELOPER)) { + MinecraftServer minecraftServer = MGZPlugin.getMinecraftServer(); + set(slot++, new Button(new ItemBuilder(XMaterial.MAP) + .setName("§6§lServer Statistics").setLore( + "§b" + minecraftServer.getName(), + "", + "§7CPU §f" + zone.themcgamer.common.MiscUtils.percent(CpuMonitor.systemLoad10SecAvg(), 1D), + "§7Memory §f" + minecraftServer.getUsedRam() + "/" + minecraftServer.getMaxRam() + " §7MB", + "§7TPS §f" + MiscUtils.formatTps(ServerUtils.getTps()), + "§7Uptime §f" + TimeUtils.formatIntoDetailedString(ManagementFactory.getRuntimeMXBean().getUptime(), false), + "§7Version §f" + Bukkit.getBukkitVersion(), + "§7Players §f" + Bukkit.getOnlinePlayers().size() + "§7/§f" + Bukkit.getMaxPlayers(), + "", + "§7Node §f" + minecraftServer.getNode().getName(), + "§7Host §f" + minecraftServer.getAddress() + ":" + minecraftServer.getPort(), + "", + "§7Click to manage profilers" + ).toItemStack(), event -> player.sendMessage("manage profilers"))); + } + set(slot, new Button(new ItemBuilder(XMaterial.FEATHER) + .setName("§6§lChat").setLore( + "", + "§7Click to manage the chat" + ).toItemStack())); + } +} diff --git a/core/src/main/java/zone/themcgamer/core/stats/Stat.java b/core/src/main/java/zone/themcgamer/core/stats/Stat.java new file mode 100644 index 0000000..b15c214 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/stats/Stat.java @@ -0,0 +1,14 @@ +package zone.themcgamer.core.stats; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +/** + * @author Braydon + */ +@AllArgsConstructor @Setter @Getter +public class Stat { + private final String id; + private long value; +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/stats/StatsManager.java b/core/src/main/java/zone/themcgamer/core/stats/StatsManager.java new file mode 100644 index 0000000..e8db411 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/stats/StatsManager.java @@ -0,0 +1,15 @@ +package zone.themcgamer.core.stats; + +import org.bukkit.plugin.java.JavaPlugin; +import zone.themcgamer.core.module.Module; +import zone.themcgamer.core.module.ModuleInfo; + +/** + * @author Braydon + */ +@ModuleInfo(name = "Stats Manager") +public class StatsManager extends Module { + public StatsManager(JavaPlugin plugin) { + super(plugin); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/task/Task.java b/core/src/main/java/zone/themcgamer/core/task/Task.java new file mode 100644 index 0000000..0354550 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/task/Task.java @@ -0,0 +1,25 @@ +package zone.themcgamer.core.task; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * @author Braydon + */ +@AllArgsConstructor @Getter +public enum Task { + TEST("test"); + + private final String id; + + /** + * Get the {@link Task} matching the given id + * @param id the id + * @return the task, othewise null + */ + public static Task match(String id) { + return Arrays.stream(values()).filter(task -> task.getId().equalsIgnoreCase(id)).findFirst().orElse(null); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/task/TaskClient.java b/core/src/main/java/zone/themcgamer/core/task/TaskClient.java new file mode 100644 index 0000000..0206a75 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/task/TaskClient.java @@ -0,0 +1,14 @@ +package zone.themcgamer.core.task; + +import lombok.Getter; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Braydon + */ +@Getter +public class TaskClient { + private final List completedTasks = new ArrayList<>(); +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/task/TaskManager.java b/core/src/main/java/zone/themcgamer/core/task/TaskManager.java new file mode 100644 index 0000000..103e4ed --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/task/TaskManager.java @@ -0,0 +1,43 @@ +package zone.themcgamer.core.task; + +import org.bukkit.plugin.java.JavaPlugin; +import zone.themcgamer.core.account.MiniAccount; +import zone.themcgamer.core.module.ModuleInfo; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Optional; +import java.util.UUID; + +/** + * @author Braydon + */ +@ModuleInfo(name = "Task Manager") +public class TaskManager extends MiniAccount { + public TaskManager(JavaPlugin plugin) { + super(plugin); + } + + @Override + public TaskClient getAccount(int accountId, UUID uuid, String name, String ip, String encryptedIp) { + return new TaskClient(); + } + + @Override + public String getQuery(int accountId, UUID uuid, String name, String ip, String encryptedIp) { + return "SELECT task FROM `tasks` WHERE `accountId` = '" + accountId + "';"; + } + + @Override + public void loadAccount(int accountId, UUID uuid, String name, String ip, String encryptedIp, ResultSet resultSet) throws SQLException { + Optional client = lookup(uuid); + if (!client.isPresent()) + return; + while (resultSet.next()) { + Task task = Task.match(resultSet.getString("task")); + if (task == null) + continue; + client.get().getCompletedTasks().add(task); + } + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/traveller/ServerTraveller.java b/core/src/main/java/zone/themcgamer/core/traveller/ServerTraveller.java new file mode 100644 index 0000000..7d2f9b1 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/traveller/ServerTraveller.java @@ -0,0 +1,141 @@ +package zone.themcgamer.core.traveller; + +import com.cryptomorin.xseries.XSound; +import net.md_5.bungee.api.chat.ComponentBuilder; +import net.md_5.bungee.api.chat.HoverEvent; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.plugin.java.JavaPlugin; +import zone.themcgamer.common.MiscUtils; +import zone.themcgamer.common.RandomUtils; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.core.module.Module; +import zone.themcgamer.core.module.ModuleInfo; +import zone.themcgamer.core.plugin.MGZPlugin; +import zone.themcgamer.data.jedis.command.JedisCommandHandler; +import zone.themcgamer.data.jedis.command.impl.ServerSendCommand; +import zone.themcgamer.data.jedis.data.ServerGroup; +import zone.themcgamer.data.jedis.data.server.MinecraftServer; +import zone.themcgamer.data.jedis.repository.RedisRepository; +import zone.themcgamer.data.jedis.repository.impl.MinecraftServerRepository; +import zone.themcgamer.data.jedis.repository.impl.ServerGroupRepository; + +import java.util.ArrayList; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * @author Braydon + */ +@ModuleInfo(name = "Server Traveller") +public class ServerTraveller extends Module { + private final ServerGroupRepository serverGroupRepository; + private final MinecraftServerRepository minecraftServerRepository; + + public ServerTraveller(JavaPlugin plugin) { + super(plugin); + serverGroupRepository = RedisRepository.getRepository(ServerGroupRepository.class).orElse(null); + minecraftServerRepository = RedisRepository.getRepository(MinecraftServerRepository.class).orElse(null); + } + + /** + * Send all players to the provided server + * @param server the server to send the players to + */ + public void sendAll(String server) { + sendAll(server, null, true); + } + + /** + * Send all players to the provided server + * @param server the server to send the players to + * @param reason the reason for sending the players + */ + public void sendAll(String server, String reason) { + sendAll(server, reason, true); + } + + /** + * Send all players to the provided server + * @param server the server to send the players to + * @param reason the reason for sending the players + * @param inform whether or not to inform the player that they are + * being sent + */ + public void sendAll(String server, String reason, boolean inform) { + if (reason != null) { + Bukkit.broadcastMessage(""); + Bukkit.broadcastMessage(Style.color(" &c➢ &7" + reason)); + Bukkit.broadcastMessage(""); + } + for (Player player : Bukkit.getOnlinePlayers()) { + try { + player.playSound(player.getEyeLocation(), XSound.ENTITY_VILLAGER_AMBIENT.parseSound(), 0.9f, 1f); + } catch (NoClassDefFoundError ignored) {} + sendPlayer(player, server, inform); + } + } + + /** + * Send the provided player to the provided server + * @param player the player to send + * @param server the name of the server to send the player to + */ + public void sendPlayer(Player player, String server) { + sendPlayer(player, server, true); + } + + /** + * Send the provided player to the provided server + * @param player the player to send + * @param server the name of the server to send the player to + * @param inform whether or not to inform the player that they are + * being sent + */ + public void sendPlayer(Player player, String server, boolean inform) { + MinecraftServer minecraftServer = null; + Optional serverGroup = serverGroupRepository.lookup(server); + if (serverGroup.isPresent() && (!serverGroup.get().getServers().isEmpty())) { + ArrayList servers = serverGroup.get().getServers().stream() + .filter(mcServer -> !mcServer.equals(MGZPlugin.getMinecraftServer()) && mcServer.isRunning()) + .collect(Collectors.toCollection(ArrayList::new)); + if (servers.size() >= 2) + minecraftServer = RandomUtils.random(servers); + else if (!servers.isEmpty()) minecraftServer = servers.get(0); + } else minecraftServer = minecraftServerRepository.lookup(mcServer -> mcServer.getName().equalsIgnoreCase(server)).orElse(null); + if (minecraftServer == null) + throw new IllegalArgumentException("Minecraft server doesn't exist"); + sendPlayer(player, minecraftServer, inform); + } + + /** + * Send the provided player to the provided {@link MinecraftServer} + * @param player the player to send + * @param server the server to send the player to + */ + public void sendPlayer(Player player, MinecraftServer server) { + sendPlayer(player, server, true); + } + + /** + * Send the provided player to the provided {@link MinecraftServer} + * @param player the player to send + * @param server the server to send the player to + * @param inform whether or not to inform the player that they are + * being sent + */ + public void sendPlayer(Player player, MinecraftServer server, boolean inform) { + if (!server.isRunning()) + throw new IllegalStateException("Server is unavailable"); + if (MGZPlugin.getMinecraftServer().equals(server)) + throw new IllegalArgumentException("Player is already connected"); + if (inform) { + player.sendMessage(new ComponentBuilder(Style.main("Traveller", "Connecting to &6" + server.getName())) + .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder(MiscUtils.arrayToString( + Style.color("&7Server Id: &6" + server.getId()), + Style.color("&7Type: &6" + server.getGroup().getName()) + )).create())).create()); + } + JedisCommandHandler.getInstance().send(new ServerSendCommand(player.getName(), server.getId())); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/update/ServerUpdater.java b/core/src/main/java/zone/themcgamer/core/update/ServerUpdater.java new file mode 100644 index 0000000..2ab3e6b --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/update/ServerUpdater.java @@ -0,0 +1,154 @@ +package zone.themcgamer.core.update; + +import lombok.NonNull; +import org.bukkit.Bukkit; +import org.bukkit.plugin.java.JavaPlugin; +import zone.themcgamer.core.module.Module; +import zone.themcgamer.core.module.ModuleInfo; +import zone.themcgamer.core.plugin.MGZPlugin; +import zone.themcgamer.core.traveller.ServerTraveller; +import zone.themcgamer.data.jedis.data.server.MinecraftServer; +import zone.themcgamer.data.jedis.data.server.ServerState; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ThreadLocalRandom; + +/** + * @author Braydon + */ +@ModuleInfo(name = "Server Updater") +public class ServerUpdater extends Module { + private static final long CHECK_DELAY = 60L * 20L; // 1 Minute + + private final ServerTraveller traveller; + + private boolean updatePendingRestart; + private long updateFoundTime; + private int restartDelay; + + public ServerUpdater(JavaPlugin plugin, ServerTraveller traveller) { + super(plugin); + this.traveller = traveller; + + // Creating the jars directory + File jarsDirectory = new File(File.separator + "home" + File.separator + "minecraft" + File.separator + "upload" + File.separator + "jars"); + if (!jarsDirectory.exists()) + jarsDirectory.mkdirs(); + + // Mapping the jar hashes for the files inside of the plugins directory + Map jarHashes = new HashMap<>(); + for (Map.Entry entry : getChecksums(new File("plugins")).entrySet()) + jarHashes.put(entry.getKey().getName(), entry.getValue()); + log("Listing jars..."); + for (Map.Entry entry : jarHashes.entrySet()) + log("'" + entry.getKey() + "' = '" + entry.getValue() + "'"); + + Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, () -> { + // If there is a pending restart and if the restart delay has passed since the restart was found, update the server + if (updatePendingRestart) { + if ((int) (((System.currentTimeMillis() - updateFoundTime)) / 60000L) >= restartDelay) + update(); + return; + } + + // Get the checksums from the update directory and compare them with the old checksums that + // we fetched before. If a checksum is different, the server will have a restart delay + // generated based on how many other servers there are opened in the same group + for (Map.Entry entry : getChecksums(jarsDirectory).entrySet()) { + String fileName = entry.getKey().getName(); + String oldChecksum = jarHashes.get(fileName); + if (oldChecksum == null) + continue; + String newChecksum = entry.getValue(); + if (!oldChecksum.equals(newChecksum)) { + updateFoundTime = System.currentTimeMillis(); + restartDelay = ThreadLocalRandom.current().nextInt(0, Math.min(MGZPlugin.getMinecraftServer().getGroup().getServers().size(), 3)); + updatePendingRestart = true; + + String timeString = (restartDelay <= 0L ? "now" : "in " + restartDelay + " minute" + (restartDelay == 1 ? "" : "s")); + log("Jar '" + fileName + "' was updated:"); + log(" Old checksum = '" + oldChecksum + "'"); + log(" New checksum = '" + newChecksum + "'"); + log(" Restarting " + timeString); + if (restartDelay <= 0L) + update(); + break; + } + jarHashes.put(fileName, newChecksum); + } + }, CHECK_DELAY, CHECK_DELAY); + } + + /** + * Mark the server as updating, send all the players to the next available server, and shutdown the server + */ + private void update() { + MinecraftServer minecraftServer = MGZPlugin.getMinecraftServer(); + try { + traveller.sendAll("Hub", "&6" + minecraftServer.getName() + " &7is being updated"); + } catch (IllegalArgumentException ignored) {} + Bukkit.getScheduler().scheduleSyncDelayedTask(getPlugin(), () -> minecraftServer.setState(ServerState.UPDATING), 10L); + Bukkit.getScheduler().scheduleSyncDelayedTask(getPlugin(), Bukkit::shutdown, 40L); + } + + /** + * Get a map of checksums from the given {@link File} directory + * @param directory the directory + * @return the map of checksums + */ + private Map getChecksums(File directory) { + Map checksums = new HashMap<>(); + if (!directory.exists()) + return checksums; + File[] files = directory.listFiles(); + if (files == null) + return checksums; + for (File file : files) { + String name = file.getName(); + if (!name.contains(".")) + continue; + String[] split = name.split("\\."); + if (split.length < 1 || (!split[split.length - 1].equalsIgnoreCase("jar"))) + continue; + Optional optionalChecksum = getChecksum(file); + if (!optionalChecksum.isPresent()) { + log("Failed to retrieve checksum for file '" + file.getAbsolutePath() + "' in directory '" + + directory.getAbsolutePath() + "', continuing..."); + continue; + } + checksums.put(file, optionalChecksum.get()); + } + return checksums; + } + + /** + * Get the checksum for the given {@link File} + * @param file the file to get the checksum for + * @return the optional checksum + */ + private Optional getChecksum(@NonNull File file) { + StringBuilder builder = new StringBuilder(); + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + FileInputStream inputStream = new FileInputStream(file); + byte[] bytes = new byte[1024]; + int read; + while ((read = inputStream.read(bytes)) != -1) + digest.update(bytes, 0, read); + byte[] mdbytes = digest.digest(); + for (byte mdbyte : mdbytes) + builder.append(Integer.toString((mdbyte & 0xff) + 0x100, 16).substring(1)); + } catch (NoSuchAlgorithmException | IOException ex) { + ex.printStackTrace(); + } + String checksum = builder.toString(); + return checksum.isEmpty() ? Optional.empty() : Optional.of(checksum); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/world/MGZWorld.java b/core/src/main/java/zone/themcgamer/core/world/MGZWorld.java new file mode 100644 index 0000000..f49598f --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/world/MGZWorld.java @@ -0,0 +1,201 @@ +package zone.themcgamer.core.world; + +import lombok.Getter; +import lombok.Setter; +import lombok.SneakyThrows; +import lombok.ToString; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.Player; + +import java.io.*; +import java.util.*; +import java.util.stream.Collectors; + +/** + * @author Braydon + */ +@Setter @Getter @ToString +public class MGZWorld { + public static final String FILE_NAME = "mgzWorld.properties"; + @Getter private static final List worlds = new ArrayList<>(); + + private World world; + private File dataFile; + private String name, originalCreator, author; + private String preset; + private WorldCategory category; + private final List admins = new ArrayList<>(); + @Setter private Map> dataPoints = new HashMap<>(); + + private MGZWorld(World world) { + this.world = world; + dataFile = new File(world.getWorldFolder(), FILE_NAME); + loadData(); + worlds.add(this); + } + + public MGZWorld(World world, File file, String name, String author, String preset, WorldCategory category) { + this.world = world; + dataFile = file; + this.name = name; + originalCreator = author; + this.author = author; + this.preset = preset; + this.category = category; + } + + @SneakyThrows + public MGZWorld(File file) { + if (file.isFile()) + dataFile = file; + else { + File dataFile = new File(file, FILE_NAME); + if (!dataFile.exists()) + throw new FileNotFoundException("Cannot read data file for map"); + else this.dataFile = dataFile; + } + loadData(); + } + + public Location getDataPoint(String name) { + List dataPoints = getDataPoints(name); + if (dataPoints.isEmpty()) + return null; + return dataPoints.get(0); + } + + public List getDataPoints(String name) { + return dataPoints.getOrDefault(name, new ArrayList<>()); + } + + public boolean hasPrivileges(Player player) { + if (player.isOp() || player.getName().equalsIgnoreCase(originalCreator)) + return true; + return isAdmin(player); + } + + public boolean isAdmin(Player player) { + return admins.contains(player.getName().toLowerCase()); + } + + public void save() { + save(dataFile); + } + + @SneakyThrows + public void save(File file) { + if (!dataFile.exists()) + dataFile.createNewFile(); + Properties properties = new Properties(); + try (OutputStream outputStream = new FileOutputStream(file)) { + properties.setProperty("name", name); + properties.setProperty("originalCreator", author); + properties.setProperty("author", author); + properties.setProperty("generator", preset == null ? WorldGenerator.FLAT.getPreset() : preset); + properties.setProperty("category", category.name()); + properties.setProperty("admins", admins.isEmpty() ? "null" : String.join(",", admins)); + + if (!dataPoints.isEmpty()) { + for (Map.Entry> entry : dataPoints.entrySet()) { + StringBuilder builder = new StringBuilder(); + for (Location location : entry.getValue()) + builder.append(locationToString(location)).append(","); + String locationsString = builder.toString(); + locationsString = locationsString.substring(0, locationsString.length() - 1); + properties.setProperty("dp-" + entry.getKey(), locationsString); + } + } + + properties.store(outputStream, null); + } + } + + @SneakyThrows + private void loadData() { + if (dataFile == null || (!dataFile.exists())) + throw new FileNotFoundException(); + try { + FileInputStream fileInputStream = new FileInputStream(dataFile); + try { + Properties properties = new Properties(); + properties.load(fileInputStream); + name = properties.getProperty("name"); + originalCreator = properties.getProperty("originalCreator"); + author = properties.getProperty("author"); + + if (properties.stringPropertyNames().contains("generator")) + preset = properties.getProperty("generator"); + else { // Legacy Worlds + if (Boolean.parseBoolean(properties.getProperty("void"))) + preset = WorldGenerator.VOID.getPreset(); + else preset = WorldGenerator.FLAT.getPreset(); + } + + category = WorldCategory.valueOf(properties.getProperty("category")); + + String adminsString = properties.getProperty("admins"); + if (!adminsString.equals("null")) + admins.addAll(Arrays.stream(adminsString.split(",")).collect(Collectors.toList())); + + for (String propertyName : properties.stringPropertyNames()) { + if (!propertyName.startsWith("dp-")) + continue; + String dataPointName = propertyName.split("-")[1]; + String propertyValue = properties.getProperty(propertyName); + if (!propertyValue.contains(",")) + dataPoints.put(dataPointName, Collections.singletonList(fromStringLocation(propertyValue))); + else { + for (String locationString : propertyValue.split(",")) { + List locations = dataPoints.getOrDefault(dataPointName, new ArrayList<>()); + locations.add(fromStringLocation(locationString)); + dataPoints.put(dataPointName, locations); + } + } + } + + } catch (IOException ex) { + ex.printStackTrace(); + } finally { + try { + fileInputStream.close(); + } catch (IOException ex) { + ex.printStackTrace(); + } + } + } catch (FileNotFoundException ex) { + ex.printStackTrace(); + } + } + + public static MGZWorld get(World world) { + MGZWorld foundWorld = worlds.stream() + .filter(mgzWorld -> mgzWorld.getWorld().getName().equals(world.getName())) + .findFirst().orElse(null); + if (foundWorld != null) + return foundWorld; + return new MGZWorld(world); + } + + private String locationToString(Location location) { + if (location == null) + return "null"; + return location.getX() + "|" + + location.getY() + "|" + + location.getZ() + "|" + + location.getYaw() + "|" + + location.getPitch(); + } + + private Location fromStringLocation(String s) { + if (s == null || (s.equals("null") || s.trim().isEmpty())) + return null; + String[] data = s.split("\\|"); + double x = Double.parseDouble(data[0]); + double y = Double.parseDouble(data[1]); + double z = Double.parseDouble(data[2]); + float yaw = Float.parseFloat(data[3]); + float pitch = Float.parseFloat(data[4]); + return new Location(world, x, y, z, yaw, pitch); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/world/WorldCategory.java b/core/src/main/java/zone/themcgamer/core/world/WorldCategory.java new file mode 100644 index 0000000..263eaeb --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/world/WorldCategory.java @@ -0,0 +1,37 @@ +package zone.themcgamer.core.world; + +import com.cryptomorin.xseries.XMaterial; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * @author Braydon + */ +@AllArgsConstructor @Getter +public enum WorldCategory { + // Games + THE_BRIDGE("The Bridge", XMaterial.END_STONE), + DISASTERS("Disasters", XMaterial.LAVA_BUCKET), + CHAOSPVP("Chaospvp", XMaterial.IRON_SWORD), + SKYBLOCK("Skyblock", XMaterial.GRASS_BLOCK), + PRISON("Prison", XMaterial.IRON_BARS), + + // Other + HUB("Hub", XMaterial.MAP), + GAME_LOBBY("Waiting Lobby", XMaterial.FILLED_MAP), + PACKS("Packs", XMaterial.OAK_SAPLING), // E.g: tree packs + PERSONAL("Personal", XMaterial.PLAYER_HEAD), + OLD("Old", XMaterial.REDSTONE_BLOCK), + OTHER("Other", XMaterial.CAULDRON); + + private final String name; + private final XMaterial icon; + + public static WorldCategory lookup(String s) { + return Arrays.stream(values()) + .filter(category -> category.name().equalsIgnoreCase(s) || category.getName().equalsIgnoreCase(s)) + .findFirst().orElse(null); + } +} \ No newline at end of file diff --git a/core/src/main/java/zone/themcgamer/core/world/WorldGenerator.java b/core/src/main/java/zone/themcgamer/core/world/WorldGenerator.java new file mode 100644 index 0000000..3614d77 --- /dev/null +++ b/core/src/main/java/zone/themcgamer/core/world/WorldGenerator.java @@ -0,0 +1,16 @@ +package zone.themcgamer.core.world; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author Braydon + */ +@AllArgsConstructor @Getter +public enum WorldGenerator { + FLAT("3;"), + VOID("3;minecraft:air;2"), + CUSTOM(null); + + private final String preset; +} \ No newline at end of file diff --git a/discordbot/build.gradle.kts b/discordbot/build.gradle.kts new file mode 100644 index 0000000..62b12ae --- /dev/null +++ b/discordbot/build.gradle.kts @@ -0,0 +1,25 @@ +dependencies { + implementation(project(":core")) + implementation("com.jagrosh:jda-utilities:3.0.5") + implementation("net.dv8tion:JDA:4.2.0_228") +} + +val jar by tasks.getting(Jar::class) { + manifest { + attributes["Main-Class"] = "zone.themcgamer.discordbot.MGZBot" + } +} + +tasks { + processResources { + val tokens = mapOf("version" to project.version) + from(sourceSets["main"].resources.srcDirs) { + filter("tokens" to tokens) + } + } + + shadowJar { + archiveFileName.set("${project.rootProject.name}-${project.name}-v${project.version}.jar") + destinationDir = file("$rootDir/output") + } +} \ No newline at end of file diff --git a/discordbot/src/main/java/zone/themcgamer/discordbot/MGZBot.java b/discordbot/src/main/java/zone/themcgamer/discordbot/MGZBot.java new file mode 100644 index 0000000..918c713 --- /dev/null +++ b/discordbot/src/main/java/zone/themcgamer/discordbot/MGZBot.java @@ -0,0 +1,60 @@ +package zone.themcgamer.discordbot; + +import com.jagrosh.jdautilities.command.CommandClientBuilder; +import com.jagrosh.jdautilities.commons.waiter.EventWaiter; +import lombok.Getter; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.JDABuilder; +import net.dv8tion.jda.api.OnlineStatus; +import net.dv8tion.jda.api.entities.Activity; +import net.dv8tion.jda.api.requests.GatewayIntent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import zone.themcgamer.discordbot.commands.BotStatusCommand; + +import javax.security.auth.login.LoginException; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +public class MGZBot { + + private static final Logger LOG = LoggerFactory.getLogger(MGZBot.class); + + @Getter private static JDA jda; + @Getter private static CommandClientBuilder commandClientBuilder; + @Getter private static EventWaiter eventWaiter; + @Getter private static ScheduledExecutorService executorService; + + public static void main(String[] args) { + long time = System.currentTimeMillis(); + eventWaiter = new EventWaiter(); + + commandClientBuilder = new CommandClientBuilder(); + commandClientBuilder.setPrefix("."); + commandClientBuilder.setActivity(Activity.playing("McGamerZone")); + commandClientBuilder.setStatus(OnlineStatus.DO_NOT_DISTURB); + commandClientBuilder.setOwnerId("504069946528104471"); + commandClientBuilder.setCoOwnerIds("504147739131641857"); + commandClientBuilder.setEmojis("<:success:789354594651209738>", "<:warning:789354594877964324>", "<:error:789354595003793408>"); + commandClientBuilder.setAlternativePrefix("/"); + commandClientBuilder.useHelpBuilder(false); + commandClientBuilder.addCommand(new BotStatusCommand(eventWaiter)); + + executorService = Executors.newScheduledThreadPool(10); + + try { + jda = JDABuilder.createDefault("ODA5NjMxMzcxNzg1Nzk3NjMz.YCX5-Q.t4S8qOmhAc98DKKw9rBsPNv82xM") + .setCallbackPool(getExecutorService()) + .setActivity(Activity.playing("loading...")) + .setStatus(OnlineStatus.IDLE) + .enableIntents(GatewayIntent.GUILD_MEMBERS, GatewayIntent.GUILD_EMOJIS) + .addEventListeners(eventWaiter, + commandClientBuilder.build()) + .build(); + } catch (LoginException e) { + e.printStackTrace(); + } + + System.out.println("Done (" + (System.currentTimeMillis() - time) + ")! For help, type \"help\" or \"?\"\n"); + } +} diff --git a/discordbot/src/main/java/zone/themcgamer/discordbot/commands/AccountCommand.java b/discordbot/src/main/java/zone/themcgamer/discordbot/commands/AccountCommand.java new file mode 100644 index 0000000..0019801 --- /dev/null +++ b/discordbot/src/main/java/zone/themcgamer/discordbot/commands/AccountCommand.java @@ -0,0 +1,45 @@ +package zone.themcgamer.discordbot.commands; + +import com.jagrosh.jdautilities.command.Command; +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.jdautilities.doc.standard.CommandInfo; +import com.jagrosh.jdautilities.doc.standard.Error; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.Message; + +import java.util.List; +import java.util.UUID; + +@CommandInfo( + name = {"account", "profile"}, + usage = "", + description = "View your own account") +@Error(prefix = "Account » ", + value = "An error occured in this command, please contact an administrator.") +public class AccountCommand extends Command { + @Override + protected void execute(CommandEvent commandEvent) { + Message message = commandEvent.getMessage(); + String[] args = message.getContentDisplay().split(" "); + + + UUID player; + List mentioned = message.getMentionedMembers(); + if (args.length > 1) { + Member target = mentioned.get(0); + //(!mentioned.isEmpty()) ? mentioned.get(0) : args[0]; + + //TODO check if account is linked + + player = UUID.randomUUID(); //"SET THE UUID"; + } else { + //TODO your own account if you did not have more than 1 args. + + player = UUID.randomUUID(); //"YOUR OWN UUID"; + } + + //TODO sent the message with player information + + } +} + diff --git a/discordbot/src/main/java/zone/themcgamer/discordbot/commands/BotStatusCommand.java b/discordbot/src/main/java/zone/themcgamer/discordbot/commands/BotStatusCommand.java new file mode 100644 index 0000000..5bf05ae --- /dev/null +++ b/discordbot/src/main/java/zone/themcgamer/discordbot/commands/BotStatusCommand.java @@ -0,0 +1,53 @@ +package zone.themcgamer.discordbot.commands; + +import com.jagrosh.jdautilities.command.Command; +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.jdautilities.commons.waiter.EventWaiter; +import com.sun.management.OperatingSystemMXBean; +import net.dv8tion.jda.api.EmbedBuilder; + +import java.awt.*; +import java.lang.management.ManagementFactory; +import java.text.DecimalFormat; +import java.time.Instant; +import java.util.Date; + +public class BotStatusCommand extends Command { + + public BotStatusCommand(EventWaiter waiter) { + this.name = "botstatus"; + //this.aliases = new String[]{"bot"}; + this.help = "view status of thee bot."; + this.ownerCommand = true; + this.guildOnly = false; + this.category = new Category("Administration"); + } + + + @Override + protected void execute(CommandEvent commandEvent) { + String title = ":information_source: Stats of **"+ commandEvent.getJDA().getSelfUser().getName()+"**:"; + String os = ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class).getName(); + String arch = ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class).getArch(); + String version = ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class).getVersion(); + os = os+" "+arch+" "+version; + int cpus = Runtime.getRuntime().availableProcessors(); + String processCpuLoad = new DecimalFormat("###.###%").format(ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class).getProcessCpuLoad()); + String systemCpuLoad = new DecimalFormat("###.###%").format(ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class).getSystemCpuLoad()); + long ramUsed = ((Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory()) / (1024 * 1024)); + + EmbedBuilder builder = new EmbedBuilder(); + builder.setTitle(title); + builder.addField(":desktop: OS:", os, true); + builder.addField(":computer: RAM usage:", ramUsed+"MB", true); + builder.addField(":gear: CPU usage:", processCpuLoad + "/" + systemCpuLoad + " (" + cpus + " Cores)", true); + builder.addField(":map: Guilds:", "" + commandEvent.getJDA().getGuilds().size() , true); + builder.addField(":speech_balloon: Text Channels:", "" + commandEvent.getJDA().getTextChannels().size(), true); + builder.addField(":speaker: Voice Channels:", "" + commandEvent.getJDA().getVoiceChannels().size(), true); + builder.addField(":bust_in_silhouette: Users:", "" + commandEvent.getJDA().getUsers().size(), true); + builder.setColor(Color.RED); + builder.setFooter("© McGamerZone - " + Date.from(Instant.now()).getYear(), commandEvent.getJDA().getSelfUser().getEffectiveAvatarUrl()); + commandEvent.getChannel().sendMessage(builder.build()).queue(); + } +} + diff --git a/discordbot/src/main/java/zone/themcgamer/discordbot/commands/StatusCommand.java b/discordbot/src/main/java/zone/themcgamer/discordbot/commands/StatusCommand.java new file mode 100644 index 0000000..b062f4a --- /dev/null +++ b/discordbot/src/main/java/zone/themcgamer/discordbot/commands/StatusCommand.java @@ -0,0 +1,53 @@ +package zone.themcgamer.discordbot.commands; + +import com.jagrosh.jdautilities.command.Command; +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.jdautilities.commons.waiter.EventWaiter; +import com.sun.management.OperatingSystemMXBean; +import net.dv8tion.jda.api.EmbedBuilder; + +import java.awt.*; +import java.lang.management.ManagementFactory; +import java.text.DecimalFormat; +import java.time.Instant; +import java.util.Date; + +public class StatusCommand extends Command { + + public StatusCommand(EventWaiter waiter) { + this.name = "setstatus"; + //this.aliases = new String[]{"bot"}; + this.help = "view status of thee bot."; + this.ownerCommand = true; + this.guildOnly = false; + this.category = new Category("Administration"); + } + + + @Override + protected void execute(CommandEvent commandEvent) { + String title = ":information_source: Stats of **"+ commandEvent.getJDA().getSelfUser().getName()+"**:"; + String os = ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class).getName(); + String arch = ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class).getArch(); + String version = ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class).getVersion(); + os = os+" "+arch+" "+version; + int cpus = Runtime.getRuntime().availableProcessors(); + String processCpuLoad = new DecimalFormat("###.###%").format(ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class).getProcessCpuLoad()); + String systemCpuLoad = new DecimalFormat("###.###%").format(ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class).getSystemCpuLoad()); + long ramUsed = ((Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory()) / (1024 * 1024)); + + EmbedBuilder builder = new EmbedBuilder(); + builder.setTitle(title); + builder.addField(":desktop: OS:", os, true); + builder.addField(":computer: RAM usage:", ramUsed+"MB", true); + builder.addField(":gear: CPU usage:", processCpuLoad + "/" + systemCpuLoad + " (" + cpus + " Cores)", true); + builder.addField(":map: Guilds:", "" + commandEvent.getJDA().getGuilds().size() , true); + builder.addField(":speech_balloon: Text Channels:", "" + commandEvent.getJDA().getTextChannels().size(), true); + builder.addField(":speaker: Voice Channels:", "" + commandEvent.getJDA().getVoiceChannels().size(), true); + builder.addField(":bust_in_silhouette: Users:", "" + commandEvent.getJDA().getUsers().size(), true); + builder.setColor(Color.RED); + builder.setFooter("© McGamerZone - " + Date.from(Instant.now()).getYear(), commandEvent.getJDA().getSelfUser().getEffectiveAvatarUrl()); + commandEvent.getChannel().sendMessage(builder.build()).queue(); + } +} + diff --git a/discordbot/src/main/java/zone/themcgamer/discordbot/commands/Test.java b/discordbot/src/main/java/zone/themcgamer/discordbot/commands/Test.java new file mode 100644 index 0000000..1d5adbf --- /dev/null +++ b/discordbot/src/main/java/zone/themcgamer/discordbot/commands/Test.java @@ -0,0 +1,35 @@ +package zone.themcgamer.discordbot.commands; + +import com.jagrosh.jdautilities.command.Command; +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.jdautilities.doc.standard.CommandInfo; +import com.jagrosh.jdautilities.doc.standard.Error; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; + +@CommandInfo( + name = {"mycommand", "coolcommand"}, + description = "Use this command if you are cool! B)", + requirements = {"The bot has all necessary permissions."}) +@Error( + prefix = "Test »", + value = "Rip this command had an error.", + response = "Invalid page number") +public class Test extends Command { + @Override + protected void execute(CommandEvent commandEvent) { + + } + + private OkHttpClient clientWithApiKey(String apiKey) { + return new OkHttpClient.Builder() + .addInterceptor(chain -> { + Request originalRequest = chain.request(); + HttpUrl newUrl = originalRequest.url().newBuilder() + .addQueryParameter("key", apiKey).build(); + Request request = originalRequest.newBuilder().url(newUrl).build(); + return chain.proceed(request); + }).build(); + } +} diff --git a/discordbot/src/main/resources/META-INF/MANIFEST.MF b/discordbot/src/main/resources/META-INF/MANIFEST.MF new file mode 100644 index 0000000..f3a4463 --- /dev/null +++ b/discordbot/src/main/resources/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: zone.themcgamer.discordbot.MGZBot + diff --git a/hub/build.gradle.kts b/hub/build.gradle.kts new file mode 100644 index 0000000..9ccdef2 --- /dev/null +++ b/hub/build.gradle.kts @@ -0,0 +1,19 @@ +dependencies { + implementation(project(":core")) + compileOnly("com.destroystokyo:paperspigot:1.12.2") + implementation("com.github.cryptomorin:XSeries:7.8.0") +} + +tasks { + processResources { + val tokens = mapOf("version" to project.version) + from(sourceSets["main"].resources.srcDirs) { + filter("tokens" to tokens) + } + } + + shadowJar { + archiveFileName.set("${project.rootProject.name}-${project.name}-v${project.version}.jar") + destinationDir = file("$rootDir/output") + } +} \ No newline at end of file diff --git a/hub/src/main/java/zone/themcgamer/hub/Hub.java b/hub/src/main/java/zone/themcgamer/hub/Hub.java new file mode 100644 index 0000000..91cc136 --- /dev/null +++ b/hub/src/main/java/zone/themcgamer/hub/Hub.java @@ -0,0 +1,60 @@ +package zone.themcgamer.hub; + +import lombok.Getter; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import zone.themcgamer.core.account.AccountManager; +import zone.themcgamer.core.chat.ChatManager; +import zone.themcgamer.core.chat.component.IChatComponent; +import zone.themcgamer.core.chat.component.impl.BasicNameComponent; +import zone.themcgamer.core.chat.component.impl.BasicRankComponent; +import zone.themcgamer.core.common.MathUtils; +import zone.themcgamer.core.common.scoreboard.ScoreboardHandler; +import zone.themcgamer.core.game.kit.KitManager; +import zone.themcgamer.core.kingdom.KingdomManager; +import zone.themcgamer.core.plugin.MGZPlugin; +import zone.themcgamer.core.plugin.Startup; +import zone.themcgamer.core.world.MGZWorld; +import zone.themcgamer.hub.command.SpawnCommand; +import zone.themcgamer.hub.listener.PlayerListener; +import zone.themcgamer.hub.listener.WorldListener; +import zone.themcgamer.hub.scoreboard.HubScoreboard; + +/** + * @author Braydon + */ +@Getter +public class Hub extends MGZPlugin { + public static Hub INSTANCE; + + private Location spawn; + + @Override + public void onEnable() { + super.onEnable(); + INSTANCE = this; + } + + @Startup + public void loadHub() { + MGZWorld world = MGZWorld.get(Bukkit.getWorlds().get(0)); + spawn = world.getDataPoint("SPAWN"); + if (spawn != null) + spawn.setYaw(MathUtils.getFacingYaw(spawn, world.getDataPoints("LOOK_AT"))); + else spawn = new Location(world.getWorld(), 0, 150, 0); + + AccountManager.addMiniAccount(new KitManager(this)); + + new PlayerListener(this); + new WorldListener(this); + new ScoreboardHandler(this, HubScoreboard.class, 3L); + + new ChatManager(this, badSportSystem, new IChatComponent[] { + new BasicRankComponent(), + new BasicNameComponent() + }); + new KingdomManager(this, traveller); + + commandManager.registerCommand(new SpawnCommand(this)); + } +} \ No newline at end of file diff --git a/hub/src/main/java/zone/themcgamer/hub/command/SpawnCommand.java b/hub/src/main/java/zone/themcgamer/hub/command/SpawnCommand.java new file mode 100644 index 0000000..9bbd084 --- /dev/null +++ b/hub/src/main/java/zone/themcgamer/hub/command/SpawnCommand.java @@ -0,0 +1,16 @@ +package zone.themcgamer.hub.command; + +import lombok.RequiredArgsConstructor; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.hub.Hub; + +@RequiredArgsConstructor +public class SpawnCommand { + private final Hub hub; + + @Command(name = "spawn", description = "Teleport you to the spawn", playersOnly = true) + public void onCommand(CommandProvider command) { + command.getPlayer().teleport(hub.getSpawn()); + } +} diff --git a/hub/src/main/java/zone/themcgamer/hub/listener/PlayerListener.java b/hub/src/main/java/zone/themcgamer/hub/listener/PlayerListener.java new file mode 100644 index 0000000..c3bcfc6 --- /dev/null +++ b/hub/src/main/java/zone/themcgamer/hub/listener/PlayerListener.java @@ -0,0 +1,256 @@ +package zone.themcgamer.hub.listener; + +import com.cryptomorin.xseries.XMaterial; +import com.cryptomorin.xseries.XSound; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.ClickEvent; +import net.md_5.bungee.api.chat.ComponentBuilder; +import net.md_5.bungee.api.chat.HoverEvent; +import org.bukkit.Bukkit; +import org.bukkit.GameMode; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.Action; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.event.entity.EntityPortalEnterEvent; +import org.bukkit.event.entity.FoodLevelChangeEvent; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.player.*; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import zone.themcgamer.core.account.Account; +import zone.themcgamer.core.account.AccountManager; +import zone.themcgamer.core.account.menu.ProfileMenu; +import zone.themcgamer.core.common.*; +import zone.themcgamer.core.world.MGZWorld; +import zone.themcgamer.data.jedis.cache.CacheRepository; +import zone.themcgamer.data.jedis.cache.impl.PlayerStatusCache; +import zone.themcgamer.data.jedis.repository.RedisRepository; +import zone.themcgamer.hub.Hub; +import zone.themcgamer.hub.menu.HubsMenu; +import zone.themcgamer.hub.menu.TravellerMenu; +import zone.themcgamer.hub.menu.cosmetics.VanityMainMenu; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +/** + * @author Braydon + */ +public class PlayerListener implements Listener { + private static final ItemStack TRAVELLER = new ItemBuilder(XMaterial.COMPASS) + .setName("§a§lTraveller §8» §7Select game") + .setLore("§7Click to teleport to a game!") + .toItemStack(); + private static final ItemStack HUB_SELECTOR = new ItemBuilder(XMaterial.BEACON) + .setName("§a§lLobbies §8» §7Select lobby") + .setLore("§7Click to view hub servers") + .toItemStack(); + private static final ItemStack COSMETICS = new ItemBuilder(XMaterial.CHEST) + .setName("§a§lVanity §8» §7Select vanity") + .setLore("§7Click to view cosmetics!") + .toItemStack(); + private static final ItemStack SETTINGS = new ItemBuilder(XMaterial.COMPARATOR) + .setName("§a§lSettings §8» §7Account settings") + .setLore("§7Click to change your settings!") + .toItemStack(); + private static final ItemBuilder PROFILE = new ItemBuilder(XMaterial.PLAYER_HEAD) + .setName("§a§lAccount §8» §7View your account") + .setLore("§7Click to view your profile!"); + + private final Hub hub; + + public PlayerListener(Hub hub) { + this.hub = hub; + Bukkit.getPluginManager().registerEvents(this, hub); + } + + @EventHandler(priority = EventPriority.HIGHEST) + private void onJoin(PlayerJoinEvent event) { + Player player = event.getPlayer(); + PlayerUtils.reset(player, true, true, GameMode.ADVENTURE); + + player.getInventory().setItem(0, TRAVELLER); + player.getInventory().setItem(1, HUB_SELECTOR); + player.getInventory().setItem(4, COSMETICS); + player.getInventory().setItem(7, SETTINGS); + player.getInventory().setItem(8, PROFILE.setSkullOwner(player.getName()).toItemStack()); + player.getInventory().setHeldItemSlot(0); + + Optional optionalAccount = AccountManager.fromCache(player.getUniqueId()); + if (optionalAccount.isEmpty()) + return; + int online = 0; + Optional cacheRepository = RedisRepository.getRepository(CacheRepository.class); + if (cacheRepository.isPresent()) + online+= cacheRepository.get().getCached().stream().filter(cacheItem -> cacheItem instanceof PlayerStatusCache).count(); + + for (int i = 0; i < 5; i++) + player.sendMessage(""); + player.sendMessage(Style.color(" Welcome &7" + optionalAccount.get().getDisplayName() + " &fto &2&lMc&6&lGamer&c&lZone")); + player.sendMessage(Style.color(" &fThere is &b" + online + " &fplayer" + (online == 1 ? "" : "s") + " online!")); + player.sendMessage(""); + + List components = new ArrayList<>(); + components.addAll(Arrays.asList(new ComponentBuilder(Style.color(" ")).create())); + components.addAll(Arrays.asList(new ComponentBuilder(Style.color("§d§lSTORE")) + .event(new ClickEvent(ClickEvent.Action.OPEN_URL,"https://store.mcgamerzone.net")) + .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder(Style.color("§dClick to visit our webstore.")).create())) + .create())); + components.addAll(Arrays.asList(new ComponentBuilder(Style.color(" §8\u25AA ")).create())); + components.addAll(Arrays.asList(new ComponentBuilder(Style.color("&e&lSITE")) + .event(new ClickEvent(ClickEvent.Action.OPEN_URL,"https://mcgamerzone.net")) + .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder(Style.color("&eClick to visit our website!")).create())) + .create())); + components.addAll(Arrays.asList(new ComponentBuilder(Style.color(" §8\u25AA ")).create())); + components.addAll(Arrays.asList(new ComponentBuilder(Style.color("&a&lVOTE")) + .event(new ClickEvent(ClickEvent.Action.OPEN_URL,"https://vote.mcgamerzone.net")) + .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder(Style.color("&aClick to vote for us!")).create())) + .create())); + components.addAll(Arrays.asList(new ComponentBuilder(Style.color(" §8\u25AA ")).create())); + components.addAll(Arrays.asList(new ComponentBuilder(Style.color("&9&lDISCORD")) + .event(new ClickEvent(ClickEvent.Action.OPEN_URL,"https://discord.mcgamerzone.net")) + .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder(Style.color("&9Click to join our community!")).create())) + .create())); + player.sendMessage(components.toArray(new BaseComponent[0])); + player.sendMessage(""); + player.sendMessage(Style.color("&7 For a list of commands, use &f/help")); + player.sendMessage(Style.color("&7 Wanna learn more about this server? Use &f/wiki")); + player.sendMessage(""); + player.playSound(player.getLocation(), XSound.BLOCK_NOTE_BLOCK_PLING.parseSound(), 1f, 1f); + + player.sendTitle(Style.color("&e&lWelcome to"), Style.color("&bThe &2&lMc&6&lGamer&c&lZone"), 20, 30, 20); + player.teleport(hub.getSpawn()); + event.setJoinMessage(null); + } + + @EventHandler + private void onInteract(PlayerInteractEvent event) { + if (event.getAction() == Action.PHYSICAL) + return; + Player player = event.getPlayer(); + ItemStack item = event.getItem(); + if (item == null) + return; + if (item.isSimilar(TRAVELLER)) + new TravellerMenu(player).open(); + else if (item.isSimilar(HUB_SELECTOR)) + new HubsMenu(player).open(); + else if (item.isSimilar(COSMETICS)) + new VanityMainMenu(player).open(); + else if (item.isSimilar(SETTINGS)) + player.sendMessage("Settings"); + else if (item.isSimilar(PROFILE.setSkullOwner(player.getName()).toItemStack())) + new ProfileMenu(player).open(); + } + + @EventHandler + private void onLaunchpad(PlayerInteractEvent event) { + if (event.getAction() == Action.PHYSICAL) { + Block block = event.getClickedBlock(); + if (block != null && (block.getType() == Material.IRON_PLATE)) { + Player player = event.getPlayer(); + event.setCancelled(true); + player.setVelocity(player.getLocation().getDirection().multiply(1D).setY(1D)); + player.playSound(player.getEyeLocation(), XSound.ENTITY_CHICKEN_EGG.parseSound(), 0.9f, 1f); + } + } + } + + @EventHandler + private void onBlockInteract(PlayerInteractEvent event) { + Block block = event.getClickedBlock(); + if (block == null) + return; + if (BlockTag.TRAPDOOR.isType(block) + || BlockTag.DOOR.isType(block) + || BlockTag.FENCE_GATE.isType(block) + || BlockTag.STORAGE.isType(block) + || BlockTag.MUSIC.isType(block) + || BlockTag.ANVIL.isType(block) + || block.getType() == Material.FURNACE + || block.getType() == Material.WORKBENCH) { + event.setCancelled(true); + } + } + + @EventHandler + private void onPortal(EntityPortalEnterEvent event) { + Entity entity = event.getEntity(); + if (entity instanceof Player) { + Player player = (Player) entity; + + MGZWorld world = MGZWorld.get(Bukkit.getWorlds().get(0)); + Location spawn = world.getDataPoint("PORTAL_SPAWN"); + if (spawn != null) + spawn.setYaw(MathUtils.getFacingYaw(spawn, world.getDataPoints("LOOK_AT"))); + else spawn = hub.getSpawn(); + player.teleport(spawn); + + new TravellerMenu(player).open(); + player.playSound(player.getEyeLocation(), XSound.ENTITY_PLAYER_LEVELUP.parseSound(), 0.9f, 1f); + } + } + + @EventHandler + private void onDamage(EntityDamageEvent event) { + Entity entity = event.getEntity(); + if (event.getCause() == EntityDamageEvent.DamageCause.VOID && entity instanceof Player) + entity.teleport(hub.getSpawn()); + event.setCancelled(true); + } + + @EventHandler + private void onBlockBreak(BlockBreakEvent event) { + if (event.getPlayer().getGameMode() != GameMode.CREATIVE) + event.setCancelled(true); + } + + @EventHandler + private void onBlockPlace(BlockPlaceEvent event) { + if (event.getPlayer().getGameMode() != GameMode.CREATIVE) + event.setCancelled(true); + } + + @EventHandler + private void onDropItem(PlayerDropItemEvent event) { + if (event.getPlayer().getGameMode() != GameMode.CREATIVE) + event.setCancelled(true); + } + + @EventHandler + private void onPickupItem(PlayerPickupItemEvent event) { + if (event.getPlayer().getGameMode() != GameMode.CREATIVE) + event.setCancelled(true); + } + + @EventHandler + private void onInventoryClick(InventoryClickEvent event) { + Player player = (Player) event.getWhoClicked(); + Inventory inventory = event.getClickedInventory(); + if (inventory == null) + return; + if (player.getInventory().equals(inventory) && player.getGameMode() != GameMode.CREATIVE) + event.setCancelled(true); + } + + @EventHandler + private void onFoodLevelChange(FoodLevelChangeEvent event) { + event.setCancelled(true); + } + + @EventHandler + private void onQuit(PlayerQuitEvent event) { + event.setQuitMessage(null); + } +} \ No newline at end of file diff --git a/hub/src/main/java/zone/themcgamer/hub/listener/WorldListener.java b/hub/src/main/java/zone/themcgamer/hub/listener/WorldListener.java new file mode 100644 index 0000000..2902570 --- /dev/null +++ b/hub/src/main/java/zone/themcgamer/hub/listener/WorldListener.java @@ -0,0 +1,72 @@ +package zone.themcgamer.hub.listener; + +import com.cryptomorin.xseries.XMaterial; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.Action; +import org.bukkit.event.block.LeavesDecayEvent; +import org.bukkit.event.entity.ExplosionPrimeEvent; +import org.bukkit.event.hanging.HangingBreakByEntityEvent; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.event.weather.WeatherChangeEvent; +import zone.themcgamer.hub.Hub; + +/** + * @author Braydon + */ +public class WorldListener implements Listener { + public WorldListener(Hub hub) { + Bukkit.getPluginManager().registerEvents(this, hub); + for (World world : Bukkit.getWorlds()) { + long time = 6000L; + if (world.getName().toLowerCase().contains("christmas")) + time = 12000L; + else if (world.getName().toLowerCase().contains("halloween")) + time = 17000L; + world.setTime(time); + world.setThundering(false); + world.setStorm(false); + world.setSpawnLocation(0, 50, 0); + world.setGameRuleValue("randomTickSpeed", "0"); + world.setGameRuleValue("doDaylightCycle", "false"); + world.setGameRuleValue("showDeathMessages", "false"); + world.setGameRuleValue("doFireTick", "false"); + world.setGameRuleValue("mobGriefing", "false"); + world.setGameRuleValue("doMobLoot", "false"); + world.setGameRuleValue("doMobSpawning", "false"); + } + } + + @EventHandler + private void onWeatherChange(WeatherChangeEvent event) { + if (event.toWeatherState()) + event.setCancelled(true); + } + + @EventHandler + private void onTnTPrime(ExplosionPrimeEvent event) { + event.setCancelled(true); + } + + @EventHandler + private void onLeaveDecay(LeavesDecayEvent event) { + event.setCancelled(true); + } + + @EventHandler + private void entityChangeSoil(PlayerInteractEvent event) { + if (event.getAction() != Action.PHYSICAL) + return; + if (event.getClickedBlock().getType() == XMaterial.FARMLAND.parseMaterial()) + event.setCancelled(true); + } + + @EventHandler + public void onHangingBreakByEntity(HangingBreakByEntityEvent event) { + if (event.getRemover() instanceof Player) + event.setCancelled(true); + } +} \ No newline at end of file diff --git a/hub/src/main/java/zone/themcgamer/hub/menu/GameKitsMenu.java b/hub/src/main/java/zone/themcgamer/hub/menu/GameKitsMenu.java new file mode 100644 index 0000000..73d06d9 --- /dev/null +++ b/hub/src/main/java/zone/themcgamer/hub/menu/GameKitsMenu.java @@ -0,0 +1,73 @@ +package zone.themcgamer.hub.menu; + +import com.cryptomorin.xseries.XSound; +import org.bukkit.entity.Player; +import zone.themcgamer.core.common.ItemBuilder; +import zone.themcgamer.core.common.menu.Button; +import zone.themcgamer.core.common.menu.Menu; +import zone.themcgamer.core.common.menu.MenuType; +import zone.themcgamer.core.game.MGZGame; +import zone.themcgamer.core.game.kit.KitClient; +import zone.themcgamer.core.game.kit.KitDisplay; +import zone.themcgamer.core.game.kit.KitManager; +import zone.themcgamer.core.module.Module; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * @author Braydon + */ +public class GameKitsMenu extends Menu { + private final MGZGame game; + + public GameKitsMenu(Player player, MGZGame game) { + super(player, game.getName() + " - Kits", 6, MenuType.DISPENSER); + this.game = game; + } + + @Override + protected void onOpen() { + List lore = new ArrayList<>(); + lore.add(""); + for (String descriptionLine : game.getDescription()) + lore.add("§7" + descriptionLine); + lore.add(""); + lore.add("§c« Click to go back"); + set(0, 1, new Button(new ItemBuilder(game.getIcon(), 1) + .setName("§6§l" + game.getName()) + .setLore(lore) + .toItemStack(), event -> new TravellerMenu(player).open())); + + KitManager kitManager = Module.getModule(KitManager.class); + if (kitManager == null) + return; + Optional optionalKitClient = kitManager.lookup(player.getUniqueId()); + if (!optionalKitClient.isPresent()) + return; + KitClient kitClient = optionalKitClient.get(); + + int slot = 3; + for (KitDisplay kit : game.getKitDisplays()) { + boolean selected = kitClient.getKit(game).equals(kit); + lore = new ArrayList<>(); + lore.add(""); + for (String descriptionLine : kit.getDescription()) + lore.add("§7" + descriptionLine); + if (selected) { + lore.add(""); + lore.add("§aSelected!"); + } + set(slot++, new Button(new ItemBuilder(kit.getIcon()) + .setName("§6§l" + kit.getName()) + .setLore(lore).toItemStack(), event -> { + if (selected) { + player.playSound(player.getEyeLocation(), XSound.ENTITY_VILLAGER_NO.parseSound(), 0.9f, 1f); + return; + } + // TODO: 1/31/21 select kit + })); + } + } +} \ No newline at end of file diff --git a/hub/src/main/java/zone/themcgamer/hub/menu/HubsMenu.java b/hub/src/main/java/zone/themcgamer/hub/menu/HubsMenu.java new file mode 100644 index 0000000..7134a5f --- /dev/null +++ b/hub/src/main/java/zone/themcgamer/hub/menu/HubsMenu.java @@ -0,0 +1,89 @@ +package zone.themcgamer.hub.menu; + +import com.cryptomorin.xseries.XMaterial; +import com.cryptomorin.xseries.XSound; +import org.bukkit.ChatColor; +import org.bukkit.entity.Player; +import zone.themcgamer.core.account.Account; +import zone.themcgamer.core.account.AccountManager; +import zone.themcgamer.core.common.ItemBuilder; +import zone.themcgamer.core.common.SkullTexture; +import zone.themcgamer.core.common.menu.Button; +import zone.themcgamer.core.common.menu.MenuPattern; +import zone.themcgamer.core.common.menu.MenuType; +import zone.themcgamer.core.common.menu.UpdatableMenu; +import zone.themcgamer.core.module.Module; +import zone.themcgamer.core.plugin.MGZPlugin; +import zone.themcgamer.core.traveller.ServerTraveller; +import zone.themcgamer.data.Rank; +import zone.themcgamer.data.jedis.data.server.MinecraftServer; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * @author Braydon + */ +public class HubsMenu extends UpdatableMenu { + public HubsMenu(Player player) { + super(player, "Hubs", 4, MenuType.CHEST); + } + + @Override + public void onUpdate() { + List slots = MenuPattern.getSlots( + "XXXXXXXXX", + "XOOOOOOOX", + "XOOOOOOOX", + "XXXXXXXXX" + ); + fillBorders(new Button(new ItemBuilder(XMaterial.BLACK_STAINED_GLASS_PANE).setName("").toItemStack())); + Optional optionalAccount = AccountManager.fromCache(player.getUniqueId()); + if (optionalAccount.isEmpty()) + return; + MinecraftServer currentServer = MGZPlugin.getMinecraftServer(); + for (MinecraftServer server : currentServer.getGroup().getServers()) { + if (!server.isRunning() || server.getNumericId() > 54) + continue; + boolean full = server.getOnline() >= server.getMaxPlayers(); + boolean canJoinFull = optionalAccount.get().hasRank(Rank.GAMER); + boolean connected = currentServer.equals(server); + + List lore = new ArrayList<>(); + lore.add(""); + lore.add("§aPlayers §8» §f" + server.getOnline() + "§7/§f" + server.getMaxPlayers()); + lore.add(""); + if (connected) + lore.add("§aConnected!"); + else lore.add(full && !canJoinFull ? "§cFull!" : "§7Click to join!"); + + ChatColor color = ChatColor.GRAY; + String texture = SkullTexture.DIAMOND_BLOCK; + if (connected) { + color = ChatColor.GREEN; + texture = SkullTexture.EMERALD_BLOCK; + } else if (full && !canJoinFull) { + color = ChatColor.RED; + texture = SkullTexture.IRON_BLOCK; + } + int slot = server.getNumericId() - 1; + if (slot >= slots.size()) + continue; + set(slots.get(slot), new Button(new ItemBuilder(XMaterial.PLAYER_HEAD) + .setSkullOwner(texture) + .setGlow(connected) + .setName(color.toString() + server.getName()) + .setLore(lore).toItemStack(), event -> { + if (connected || (full && !canJoinFull)) { + player.playSound(player.getEyeLocation(), XSound.ENTITY_VILLAGER_NO.parseSound(), 0.9f, 1f); + return; + } + close(); + ServerTraveller traveller = Module.getModule(ServerTraveller.class); + if (traveller != null) + traveller.sendPlayer(player, server.getName()); + })); + } + } +} \ No newline at end of file diff --git a/hub/src/main/java/zone/themcgamer/hub/menu/TravellerMenu.java b/hub/src/main/java/zone/themcgamer/hub/menu/TravellerMenu.java new file mode 100644 index 0000000..0f1bc38 --- /dev/null +++ b/hub/src/main/java/zone/themcgamer/hub/menu/TravellerMenu.java @@ -0,0 +1,160 @@ +package zone.themcgamer.hub.menu; + +import com.cryptomorin.xseries.XMaterial; +import org.bukkit.entity.Player; +import zone.themcgamer.core.common.ItemBuilder; +import zone.themcgamer.core.common.SkullTexture; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.core.common.menu.Button; +import zone.themcgamer.core.common.menu.MenuPattern; +import zone.themcgamer.core.common.menu.MenuType; +import zone.themcgamer.core.common.menu.UpdatableMenu; +import zone.themcgamer.core.game.MGZGame; +import zone.themcgamer.core.module.Module; +import zone.themcgamer.core.traveller.ServerTraveller; +import zone.themcgamer.data.jedis.data.server.MinecraftServer; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * @author Braydon + */ +public class TravellerMenu extends UpdatableMenu { + private static final String[] ARROW_COLORS = new String[] { "§2", "§6", "§c" }; + + private int arrowIndex, randomGameIndex; + + public TravellerMenu(Player player) { + super(player, "Traveller", 6, MenuType.CHEST, 700L); + } + + @Override + protected void onOpen() { + fill(new Button(new ItemBuilder(XMaterial.GRAY_STAINED_GLASS_PANE).setName("&f").toItemStack())); + fillRow(0, new Button(new ItemBuilder(XMaterial.BLACK_STAINED_GLASS_PANE).setName("&f").toItemStack())); + set(0, new Button(new ItemBuilder(XMaterial.OAK_SIGN).setName("&e&lHelpful").toItemStack())); + set(4, new Button(new ItemBuilder(XMaterial.OAK_SIGN).setName("&e&lGamemodes").toItemStack())); + set(8, new Button(new ItemBuilder(XMaterial.OAK_SIGN).setName("&e&lLobby Spots").toItemStack())); + + set(1, 8, new Button(new ItemBuilder(XMaterial.CHEST).setName("&6&lGem Boxes").toItemStack())); + set(2, 8, new Button(new ItemBuilder(XMaterial.FISHING_ROD).setName("&c&lDuels").toItemStack())); + set(3, 8, new Button(new ItemBuilder(XMaterial.LEATHER_BOOTS).setName("&e&lParkour").toItemStack())); + set(4, 8, new Button(new ItemBuilder(XMaterial.SNOWBALL).setName("&a&lPaintball").toItemStack())); + + set(1, 0, new Button(new ItemBuilder(XMaterial.BOOK) + .setName("&b/help") + .addLoreLine("&7Get a list with commands") + .addLoreLine("&7that you can use. And a link") + .addLoreLine("&7to our forum with a lot of helpful page's!") + .addLoreLine("") + .addLoreLine("&aClick to get help!") + .toItemStack(), event -> { + player.performCommand("help"); + player.sendMessage(Style.color("&a&l(!) &e&lClick&7 this link to visit our forums: &amcgamerzone.net/help")); + })); + set(2, 0, new Button(new ItemBuilder(XMaterial.PLAYER_HEAD) + .setSkullOwner(SkullTexture.DISCORD) + .setName("&b/discord") + .addLoreLine("&7A discord server for gamers to chat") + .addLoreLine("&7with other &czoners&7. And be the first") + .addLoreLine("&7to &eread &7our &6updates&7, &dgiveaways&7 & &a&lmore&7!") + .addLoreLine("") + .addLoreLine("&aClick to join our discord!") + .toItemStack(), event -> { + player.performCommand("discord"); + })); + set(3, 0, new Button(new ItemBuilder(XMaterial.DIAMOND) + .setName("&b/store") + .addLoreLine("&7Purchase some goodies") + .addLoreLine("&7from our webstore and support the server!") + .addLoreLine("&7We have &eranks&7, &dbundles&7, &6and much more&7!") + .addLoreLine("") + .addLoreLine("&aClick to visit our store!") + .toItemStack(), event -> { + player.performCommand("store"); + })); + set(4, 0, new Button(new ItemBuilder(XMaterial.WRITTEN_BOOK) + .setName("&b/vote") + .addLoreLine("&eVote for us!") + .addLoreLine("&7Receive a lot of cool &drewards&7, and it helps us too!") + .addLoreLine("") + .addLoreLine("&aClick to vote for us!") + .toItemStack(), event -> { + player.performCommand("vote"); + })); + } + + @Override + public void onUpdate() { + List slots = MenuPattern.getSlots( + "XXXXXXXXX", + "XXXOOOXXX", + "XXXOOOXXX", + "XXXOOOXXX", + "XXXXXXXXX", + "XXXXXXXXX" + ); + fill(slots,new Button(new ItemBuilder(XMaterial.BARRIER).setName("&c???").toItemStack())); + if (++arrowIndex >= ARROW_COLORS.length) + arrowIndex = 0; + int index = 0; + for (MGZGame game : MGZGame.values()) { + int playing = game.getPlaying(); + boolean hasKits = game.getKitDisplays() != null && (game.getKitDisplays().length > 0); + + List lore = new ArrayList<>(); + lore.add("§8Category: §b" + game.getGameCategory().getName()); + lore.add(""); + for (String descriptionLine : game.getDescription()) + lore.add("§7" + descriptionLine); + lore.add(""); + if (hasKits) { + lore.add("§eRight-Click to view kits"); + lore.add(""); + } + lore.add((ARROW_COLORS[arrowIndex]) + "► §7Click to play with §f" + playing + " §7other player" + (playing == 1 ? "" : "s")); + set(slots.get(index++), new Button(new ItemBuilder(game.getIcon(), 1) + .setName("§6§l" + game.getName()) + .setLore(lore) + .toItemStack(), event -> { + if (event.isRightClick() && hasKits) { + new GameKitsMenu(player, game).open(); + return; + } + close(); + sendToGame(game); + })); + } + if (++randomGameIndex >= MGZGame.values().length) + randomGameIndex = 0; + MGZGame game = MGZGame.values()[randomGameIndex]; + + List lore = new ArrayList<>(); + lore.add(""); + for (MGZGame mgzGame : MGZGame.values()) + lore.add((mgzGame == game ? "§6► §f" : "§7 ") + mgzGame.getName()); + lore.add(""); + lore.add("§7Click to play a random game"); + set(4, 4, new Button(new ItemBuilder(game.getIcon(), 1) + .setName("§6Join a random game") + .setLore(lore) + .toItemStack(), event -> { + close(); + sendToGame(game); + })); + + } + + private void sendToGame(MGZGame game) { + Optional optionalMinecraftServer = game.getBestServer().filter(MinecraftServer::isRunning); + if (optionalMinecraftServer.isEmpty()) { + player.sendMessage(Style.error("Traveller", "§7There is no available game server found, please try again in a moment.")); + return; + } + ServerTraveller traveller = Module.getModule(ServerTraveller.class); + if (traveller != null) + traveller.sendPlayer(player, optionalMinecraftServer.get()); + } +} \ No newline at end of file diff --git a/hub/src/main/java/zone/themcgamer/hub/menu/cosmetics/VanityMainMenu.java b/hub/src/main/java/zone/themcgamer/hub/menu/cosmetics/VanityMainMenu.java new file mode 100644 index 0000000..d43a3bc --- /dev/null +++ b/hub/src/main/java/zone/themcgamer/hub/menu/cosmetics/VanityMainMenu.java @@ -0,0 +1,20 @@ +package zone.themcgamer.hub.menu.cosmetics; + +import com.cryptomorin.xseries.XMaterial; +import org.bukkit.entity.Player; +import zone.themcgamer.core.common.ItemBuilder; +import zone.themcgamer.core.common.Style; +import zone.themcgamer.core.common.menu.Button; +import zone.themcgamer.core.common.menu.Menu; +import zone.themcgamer.core.common.menu.MenuType; + +public class VanityMainMenu extends Menu { + public VanityMainMenu(Player player) { + super(player, "Vanity » Menu", 3, MenuType.CHEST); + } + + @Override + protected void onOpen() { + set(1,4, new Button(new ItemBuilder(XMaterial.BARRIER).setName(Style.color("&c&lComing Soon!")).toItemStack())); + } +} diff --git a/hub/src/main/java/zone/themcgamer/hub/scoreboard/HubScoreboard.java b/hub/src/main/java/zone/themcgamer/hub/scoreboard/HubScoreboard.java new file mode 100644 index 0000000..3093d2d --- /dev/null +++ b/hub/src/main/java/zone/themcgamer/hub/scoreboard/HubScoreboard.java @@ -0,0 +1,68 @@ +package zone.themcgamer.hub.scoreboard; + +import org.bukkit.ChatColor; +import org.bukkit.entity.Player; +import zone.themcgamer.common.DoubleUtils; +import zone.themcgamer.core.account.Account; +import zone.themcgamer.core.account.AccountManager; +import zone.themcgamer.core.animation.impl.WaveAnimation; +import zone.themcgamer.core.common.scoreboard.WritableScoreboard; +import zone.themcgamer.core.plugin.MGZPlugin; +import zone.themcgamer.data.jedis.cache.CacheRepository; +import zone.themcgamer.data.jedis.cache.impl.PlayerStatusCache; +import zone.themcgamer.data.jedis.repository.RedisRepository; + +import java.time.LocalDateTime; +import java.util.Optional; + +/** + * @author Braydon + */ +public class HubScoreboard extends WritableScoreboard { + private WaveAnimation title; + + public HubScoreboard(Player player) { + super(player); + } + + @Override + public String getTitle() { + if (title == null) { + title = new WaveAnimation("McGamerZone") + .withPrimary(ChatColor.GREEN.toString()) + .withSecondary(ChatColor.GOLD.toString()) + .withTertiary(ChatColor.RED.toString()) + .withBold(); + } + return title.next(); + } + + @Override + public void writeLines() { + Optional optionalAccount = AccountManager.fromCache(player.getUniqueId()); + if (!optionalAccount.isPresent()) { + writeBlank(); + return; + } + Account account = optionalAccount.get(); + + int online = 0; + Optional cacheRepository = RedisRepository.getRepository(CacheRepository.class); + if (cacheRepository.isPresent()) + online+= cacheRepository.get().getCached().stream().filter(cacheItem -> cacheItem instanceof PlayerStatusCache).count(); + + LocalDateTime dateTime = LocalDateTime.now(); + + write("§7" + dateTime.getMonth().getValue() + "/" + dateTime.getDayOfMonth() + "/" + dateTime.getYear()); + writeBlank(); + write("§fRank: &7" + account.getPrimaryRank().getColor() + account.getPrimaryRank().getDisplayName()); + write("&fGold: &6" + DoubleUtils.format(account.getGold(), true) + " \u26C3"); + write("&fGems: &2" + DoubleUtils.format(account.getGems(), true) + " \u2726"); + write("&fLevel: &b0 &a||||&7|||||||||||"); // TODO: 1/15/21 this is static for now until the stats system is completed + writeBlank(); + write("§fLobby: &a#" + MGZPlugin.getMinecraftServer().getNumericId()); + write("§fPlayers: &a" + online); + writeBlank(); + write("§ethemcgamer.zone"); + } +} \ No newline at end of file diff --git a/hub/src/main/resources/plugin.yml b/hub/src/main/resources/plugin.yml new file mode 100644 index 0000000..5e0a4a4 --- /dev/null +++ b/hub/src/main/resources/plugin.yml @@ -0,0 +1,5 @@ +name: Hub +version: 1.0-SNAPSHOT +api-version: 1.13 +main: zone.themcgamer.hub.Hub +author: MGZ Development Team \ No newline at end of file diff --git a/lombok.config b/lombok.config new file mode 100644 index 0000000..734e4cc --- /dev/null +++ b/lombok.config @@ -0,0 +1,6 @@ +# This entry is generated by the 'io.freefair.lombok' Gradle plugin +config.stopBubbling = true + +# InsaneMC +lombok.addNullAnnotations = jetbrains +lombok.addLombokGeneratedAnnotation = true diff --git a/proxy/build.gradle.kts b/proxy/build.gradle.kts new file mode 100644 index 0000000..e2776b8 --- /dev/null +++ b/proxy/build.gradle.kts @@ -0,0 +1,23 @@ +repositories { + mavenCentral() + maven(url = "https://papermc.io/repo/repository/maven-public/") +} + +dependencies { + api(project(":serverdata")) + compileOnly("io.github.waterfallmc:waterfall-api:1.16-R0.4-SNAPSHOT") +} + +tasks { + processResources { + val tokens = mapOf("version" to project.version) + from(sourceSets["main"].resources.srcDirs) { + filter("tokens" to tokens) + } + } + + shadowJar { + archiveFileName.set("${project.rootProject.name}-${project.name}-v${project.version}.jar") + destinationDir = file("$rootDir/output") + } +} \ No newline at end of file diff --git a/proxy/src/main/java/zone/themcgamer/proxy/Proxy.java b/proxy/src/main/java/zone/themcgamer/proxy/Proxy.java new file mode 100644 index 0000000..d795e2f --- /dev/null +++ b/proxy/src/main/java/zone/themcgamer/proxy/Proxy.java @@ -0,0 +1,52 @@ +package zone.themcgamer.proxy; + +import lombok.Getter; +import net.md_5.bungee.api.config.ServerInfo; +import net.md_5.bungee.api.plugin.Plugin; +import zone.themcgamer.data.jedis.JedisController; +import zone.themcgamer.proxy.hub.HubBalancer; +import zone.themcgamer.proxy.motd.MOTDHandler; +import zone.themcgamer.proxy.player.PlayerHandler; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * @author Braydon + */ +@Getter +public class Proxy extends Plugin { + private ServerInfo defaultServer; + private ProxyData proxyData; + + @Override + public void onEnable() { + // Setting the default server + Map serversMap = getProxy().getServersCopy(); + if (!serversMap.isEmpty()) { + defaultServer = new ArrayList<>(serversMap.values()).get(0); + System.out.println("defaultServer = " + defaultServer.getName()); + } else { + System.err.println("Cannot find default server"); + getProxy().stop(); + return; + } + // Initializing Redis + JedisController controller = new JedisController(); + ProxyDataRepository repository = new ProxyDataRepository(controller); + controller.start(); + getProxy().getScheduler().schedule(this, () -> { + List cached = repository.getCached(); + if (cached.isEmpty()) + proxyData = null; + else proxyData = cached.get(0); + }, 2L, 30L, TimeUnit.SECONDS); + + // Initializing Modules + new MOTDHandler(this); + new PlayerHandler(this); + new HubBalancer(this); + } +} \ No newline at end of file diff --git a/proxy/src/main/java/zone/themcgamer/proxy/ProxyData.java b/proxy/src/main/java/zone/themcgamer/proxy/ProxyData.java new file mode 100644 index 0000000..a459543 --- /dev/null +++ b/proxy/src/main/java/zone/themcgamer/proxy/ProxyData.java @@ -0,0 +1,24 @@ +package zone.themcgamer.proxy; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author Braydon + */ +@AllArgsConstructor @Getter +public class ProxyData { + private final MOTD motd; + private final boolean maintenance; + private final TABLIST tablist; + + @AllArgsConstructor @Getter + public static class MOTD { + private final String header, text; + } + + @AllArgsConstructor @Getter + public static class TABLIST { + private final String header, footer; + } +} \ No newline at end of file diff --git a/proxy/src/main/java/zone/themcgamer/proxy/ProxyDataRepository.java b/proxy/src/main/java/zone/themcgamer/proxy/ProxyDataRepository.java new file mode 100644 index 0000000..bbf572b --- /dev/null +++ b/proxy/src/main/java/zone/themcgamer/proxy/ProxyDataRepository.java @@ -0,0 +1,52 @@ +package zone.themcgamer.proxy; + +import zone.themcgamer.data.jedis.JedisController; +import zone.themcgamer.data.jedis.repository.RedisRepository; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * @author Braydon + */ +public class ProxyDataRepository extends RedisRepository { + public ProxyDataRepository(JedisController controller) { + super(controller, "proxy"); + } + + @Override + public Optional lookup(String name) { + return Optional.empty(); + } + + @Override + public String getKey(ProxyData proxyData) { + return "proxy"; + } + + @Override + public Optional fromMap(Map map) { + return Optional.of(new ProxyData( + new ProxyData.MOTD(map.get("motd.header"), map.get("motd.text")), + Boolean.parseBoolean(map.get("maintenance")), + new ProxyData.TABLIST(map.get("tablist.header"), map.get("tablist.footer")) + )); + } + + @Override + public long getExpiration(ProxyData proxyData) { + return -1; + } + + @Override + public Map toMap(ProxyData proxyData) { + Map data = new HashMap<>(); + data.put("motd.header", proxyData.getMotd().getHeader()); + data.put("motd.text", proxyData.getMotd().getText()); + data.put("maintenance", proxyData.isMaintenance()); + data.put("tablist.header", proxyData.getTablist().getHeader()); + data.put("tablist.footer", proxyData.getTablist().getFooter()); + return data; + } +} \ No newline at end of file diff --git a/proxy/src/main/java/zone/themcgamer/proxy/hub/HubBalancer.java b/proxy/src/main/java/zone/themcgamer/proxy/hub/HubBalancer.java new file mode 100644 index 0000000..fb37ab5 --- /dev/null +++ b/proxy/src/main/java/zone/themcgamer/proxy/hub/HubBalancer.java @@ -0,0 +1,213 @@ +package zone.themcgamer.proxy.hub; + +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.chat.TextComponent; +import net.md_5.bungee.api.config.ServerInfo; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.event.ServerConnectEvent; +import net.md_5.bungee.api.event.ServerKickEvent; +import net.md_5.bungee.api.plugin.Listener; +import net.md_5.bungee.event.EventHandler; +import net.md_5.bungee.event.EventPriority; +import zone.themcgamer.common.Tuple; +import zone.themcgamer.data.jedis.command.JedisCommandHandler; +import zone.themcgamer.data.jedis.command.impl.ServerStateChangeCommand; +import zone.themcgamer.data.jedis.data.ServerGroup; +import zone.themcgamer.data.jedis.data.server.MinecraftServer; +import zone.themcgamer.data.jedis.data.server.ServerState; +import zone.themcgamer.data.jedis.repository.RedisRepository; +import zone.themcgamer.data.jedis.repository.impl.MinecraftServerRepository; +import zone.themcgamer.data.jedis.repository.impl.ServerGroupRepository; +import zone.themcgamer.proxy.Proxy; + +import java.net.InetSocketAddress; +import java.util.*; +import java.util.concurrent.TimeUnit; + +/** + * @author Braydon + */ +public class HubBalancer implements Runnable, Listener { + private static final String NO_AVAILABLE_HUB = "&2&lMc&6&lGamer&c&lZone &8» &7There are no available lobbies found!"; + private static final String HUB_SEND_FAILED = "&2&lMc&6&lGamer&c&lZone &8» &cAn error occured while sending you to a hub!"; + + private final Proxy proxy; + private ServerGroupRepository serverGroupRepository; + private MinecraftServerRepository minecraftServerRepository; + private final Map hubs = new HashMap<>(); + + private ServerGroup group; + + public HubBalancer(Proxy proxy) { + this.proxy = proxy; + RedisRepository.getRepository(ServerGroupRepository.class).ifPresent(repository -> serverGroupRepository = repository); + RedisRepository.getRepository(MinecraftServerRepository.class).ifPresent(repository -> minecraftServerRepository = repository); + proxy.getProxy().getScheduler().schedule(proxy, this, 1000L, 350L, TimeUnit.MILLISECONDS); + proxy.getProxy().getPluginManager().registerListener(proxy, this); + + JedisCommandHandler.getInstance().addListener(jedisCommand -> { + if (jedisCommand instanceof ServerStateChangeCommand) { + ServerStateChangeCommand serverStateChangeCommand = (ServerStateChangeCommand) jedisCommand; + System.out.println("Received update status from server " + serverStateChangeCommand.getServer().getId() + " status: " + serverStateChangeCommand.getNewState()); + if (serverStateChangeCommand.getNewState().equals(ServerState.RUNNING)) + registerServer(serverStateChangeCommand.getServer()); + if (!serverStateChangeCommand.getNewState().isShuttingDownState()) + return; + MinecraftServer server = serverStateChangeCommand.getServer(); + proxy.getProxy().getServers().entrySet().removeIf(entry -> { + String key = entry.getKey(); + if (!key.equals(server.getId())) + return false; + if (key.equals(group.getName())) + return false; + if (entry.getValue().getMotd().equals("STATIC")) + return false; + hubs.remove(server.getId()); + return true; + }); + } + }); + } + + @EventHandler(priority = EventPriority.LOWEST) + public void onLogin(ServerConnectEvent event) { + if (event.getReason() != ServerConnectEvent.Reason.JOIN_PROXY) + return; + if (group == null) // If the server group has yet to be pulled from the repository, return + return; + ProxiedPlayer player = event.getPlayer(); + ServerConnectEvent.Reason reason = event.getReason(); + + // Debugging + System.out.println("reason = " + reason.name() + ", hubs = " + hubs.size()); + + // If the player is joining the proxy, and the target server is the name of the server group, we wanna + // find a hub for them to connect to + if (reason == ServerConnectEvent.Reason.JOIN_PROXY && event.getTarget().getName().equals(group.getName())) { + ServerInfo serverInfo = sendToHub(player); + if (serverInfo == null) { + kickPlayer(player, HUB_SEND_FAILED); + event.setCancelled(true); + } else event.setTarget(serverInfo); + } else if (reason == ServerConnectEvent.Reason.LOBBY_FALLBACK) { + kickPlayer(player, NO_AVAILABLE_HUB); + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.LOWEST) + public void onKick(ServerKickEvent event) { + System.out.println(event.getCause()); + System.out.println(event.getKickReason()); + System.out.println(event.getKickedFrom().getName()); + ProxiedPlayer player = event.getPlayer(); + if (player.getServer() == null) + return; + ServerInfo serverInfo = sendToHub(player); + if (serverInfo == null) { + kickPlayer(player, HUB_SEND_FAILED + "\n&f" + TextComponent.toLegacyText(event.getKickReasonComponent())); + event.setCancelled(true); + return; + } + event.setCancelServer(serverInfo); + event.setCancelled(true); + sendKickMessage(player, event.getKickedFrom(), TextComponent.toLegacyText(event.getKickReasonComponent())); + } + + @Override + public void run() { + Map serversMap = proxy.getProxy().getServersCopy(); + // If the group is null, we wanna get the group name from the default Bungeecord configuration file. + // The default server for BungeeCord is the name of the server group to use for fallback servers + if (group == null) + group = serverGroupRepository.lookup(proxy.getDefaultServer().getName()).orElse(null); + if (group == null) + return; + // Adding new Minecraft servers to the proxy + for (MinecraftServer server : minecraftServerRepository.getCached()) { + if (server.isRunning() && serversMap.get(server.getId()) == null) { + registerServer(server); + } + } + // Removing dead Minecraft servers from the proxy + proxy.getProxy().getServers().entrySet().removeIf(entry -> { + String key = entry.getKey(); + if (key.equals(group.getName())) + return false; + if (entry.getValue().getMotd().equals("STATIC")) + return false; + Optional optionalMinecraftServer = minecraftServerRepository.getCached().stream() + .filter(server -> server.getId().equals(key)) + .findFirst(); + if (!optionalMinecraftServer.isPresent()) { + hubs.remove(key); + return true; + } + MinecraftServer minecraftServer = optionalMinecraftServer.get(); + if (!minecraftServer.isRunning()) { + hubs.remove(minecraftServer.getId()); + return true; + } + return false; + }); + } + + private ServerInfo sendToHub(ProxiedPlayer player) { + if (hubs.isEmpty()) { // If there are no hubs available, deny the connection and show an error to the connecting player + kickPlayer(player, NO_AVAILABLE_HUB); + return null; + } + ServerInfo target = null; + if (hubs.size() == 1) // If there is only 1 hub server, there's no need to balance + target = new ArrayList<>(hubs.values()).get(0); + else { + // Finding the best possible hub to connect the player to. This is done by getting the server with + // the least amount of players online + List> playerCounts = new ArrayList<>(); + hubs.values().stream().map(serverInfo -> { + Optional optionalMinecraftServer = group.getServers().stream() + .filter(server -> server.getId().equals(serverInfo.getName())) + .findFirst(); + return optionalMinecraftServer.orElse(null); + }).filter(Objects::nonNull) + .sorted(Comparator.comparingInt(MinecraftServer::getOnline)) + .forEach(minecraftServer -> playerCounts.add(new Tuple<>(minecraftServer, minecraftServer.getOnline()))); + if (!playerCounts.isEmpty()) + target = hubs.get(playerCounts.get(0).getLeft().getId()); + } + if (target != null) { + if (player.getServer() != null && (player.getServer().getInfo().equals(target))) { + kickPlayer(player, NO_AVAILABLE_HUB); + return null; + } + System.out.println("Sending " + player.getName() + " to server \"" + target.getName() + "\""); + return target; + } + System.err.println("Cannot find a server to send " + player.getName() + " to!"); + kickPlayer(player, NO_AVAILABLE_HUB); + return null; + } + + private void registerServer(MinecraftServer minecraftServer) { + ServerInfo serverInfo = proxy.getProxy().constructServerInfo(minecraftServer.getId(), + new InetSocketAddress(group.getPrivateAddress(), (int) minecraftServer.getPort()), "A MGZ Server", false); + proxy.getProxy().getServers().put(minecraftServer.getId(), serverInfo); + if (group.getServers().contains(minecraftServer)) + hubs.put(minecraftServer.getId(), serverInfo); + } + + private void kickPlayer(ProxiedPlayer player, String string) { + player.disconnect(TextComponent.fromLegacyText( + ChatColor.translateAlternateColorCodes('&', string))); + } + + //This just only sends the kick player when player is moved back to lobby. + private void sendKickMessage(ProxiedPlayer player, ServerInfo server, String reason) { + player.sendMessage(TextComponent.fromLegacyText(ChatColor.translateAlternateColorCodes('&', + "&7\n" + + "&c\u27A2 Unexpected? Contact an administrator!\n" + + "&7You have been disconnected from " + (server == null ? "" : "from §6" + server.getName() + "§7 ") + " &7server for\n" + + "&b" + reason + "\n" + + "&7"))); + } +} \ No newline at end of file diff --git a/proxy/src/main/java/zone/themcgamer/proxy/motd/MOTDHandler.java b/proxy/src/main/java/zone/themcgamer/proxy/motd/MOTDHandler.java new file mode 100644 index 0000000..ede8314 --- /dev/null +++ b/proxy/src/main/java/zone/themcgamer/proxy/motd/MOTDHandler.java @@ -0,0 +1,41 @@ +package zone.themcgamer.proxy.motd; + +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.ServerPing; +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 zone.themcgamer.proxy.Proxy; +import zone.themcgamer.proxy.ProxyData; + +/** + * @author Braydon + */ +public class MOTDHandler implements Listener { + private static final String DEFAULT_HEADER = "§2§lMc§6§lGamer§c§lZone"; + + private final Proxy proxy; + + public MOTDHandler(Proxy proxy) { + this.proxy = proxy; + proxy.getProxy().getPluginManager().registerListener(proxy, this); + } + + @EventHandler + public void onPing(ProxyPingEvent event) { + ProxyData data = proxy.getProxyData(); + ServerPing response = event.getResponse(); + + // MOTD + if (data == null) + response.setDescriptionComponent(new TextComponent(DEFAULT_HEADER)); + else response.setDescriptionComponent(new TextComponent(ChatColor.translateAlternateColorCodes('&', data.getMotd().getHeader() + "\n" + data.getMotd().getText()))); + + // Maintenance Display + if (data != null && (data.isMaintenance())) + response.setVersion(new ServerPing.Protocol("§4Maintenance", -1)); + + event.setResponse(response); + } +} \ No newline at end of file diff --git a/proxy/src/main/java/zone/themcgamer/proxy/player/PlayerHandler.java b/proxy/src/main/java/zone/themcgamer/proxy/player/PlayerHandler.java new file mode 100644 index 0000000..a90ec5d --- /dev/null +++ b/proxy/src/main/java/zone/themcgamer/proxy/player/PlayerHandler.java @@ -0,0 +1,162 @@ +package zone.themcgamer.proxy.player; + +import net.md_5.bungee.api.chat.TextComponent; +import net.md_5.bungee.api.config.ServerInfo; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.event.PlayerDisconnectEvent; +import net.md_5.bungee.api.event.PreLoginEvent; +import net.md_5.bungee.api.event.ServerConnectEvent; +import net.md_5.bungee.api.plugin.Listener; +import net.md_5.bungee.event.EventHandler; +import net.md_5.bungee.event.EventPriority; +import zone.themcgamer.common.MiscUtils; +import zone.themcgamer.data.jedis.cache.CacheRepository; +import zone.themcgamer.data.jedis.cache.impl.PlayerStatusCache; +import zone.themcgamer.data.jedis.command.JedisCommandHandler; +import zone.themcgamer.data.jedis.command.impl.*; +import zone.themcgamer.data.jedis.data.server.MinecraftServer; +import zone.themcgamer.data.jedis.repository.RedisRepository; +import zone.themcgamer.data.jedis.repository.impl.MinecraftServerRepository; +import zone.themcgamer.proxy.Proxy; + +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +/** + * @author Braydon + */ +public class PlayerHandler implements Listener { + private final Proxy proxy; + private CacheRepository cacheRepository; + + public PlayerHandler(Proxy proxy) { + this.proxy = proxy; + proxy.getProxy().getPluginManager().registerListener(proxy, this); + + Optional optionalCacheRepository = RedisRepository.getRepository(CacheRepository.class); + optionalCacheRepository.ifPresent(repository -> cacheRepository = repository); + + Optional optionalServerRepository = RedisRepository.getRepository(MinecraftServerRepository.class); + if (optionalServerRepository.isEmpty()) + return; + MinecraftServerRepository serverRepository = optionalServerRepository.get(); + JedisCommandHandler.getInstance().addListener(jedisCommand -> { + if (jedisCommand instanceof ServerSendCommand) { + ServerSendCommand serverSendCommand = (ServerSendCommand) jedisCommand; + ProxiedPlayer player = proxy.getProxy().getPlayer(serverSendCommand.getPlayerName()); + if (player == null) + return; + Optional optionalMinecraftServer = serverRepository.lookup(serverSendCommand.getServerId()); + if (optionalMinecraftServer.isEmpty()) + return; + MinecraftServer minecraftServer = optionalMinecraftServer.get(); + ServerInfo serverInfo = proxy.getProxy().getServersCopy().get(minecraftServer.getId()); + if (serverInfo == null) + return; + player.connect(serverInfo); + } else if (jedisCommand instanceof PlayerKickCommand) { + PlayerKickCommand kickCommand = (PlayerKickCommand) jedisCommand; + ProxiedPlayer player = proxy.getProxy().getPlayer(kickCommand.getUuid()); + if (player != null) + player.disconnect(TextComponent.fromLegacyText(kickCommand.getReason())); + } else if (jedisCommand instanceof PlayerMessageCommand) { + PlayerMessageCommand playerMessageCommand = (PlayerMessageCommand) jedisCommand; + ProxiedPlayer player = proxy.getProxy().getPlayer(playerMessageCommand.getUuid()); + if (player != null) + player.sendMessage(TextComponent.fromLegacyText(playerMessageCommand.getMessage())); + } + }); + + proxy.getProxy().getScheduler().schedule(proxy, () -> { + int online = Math.toIntExact(cacheRepository.getCached().stream().filter(cacheItem -> cacheItem instanceof PlayerStatusCache).count()); + for (ProxiedPlayer player : proxy.getProxy().getPlayers()) { + Optional optionalPlayerStatusCache = cacheRepository.lookup(PlayerStatusCache.class, player.getUniqueId()); + if (optionalPlayerStatusCache.isEmpty()) { + System.err.println("Cannot remove player status for " + player.getName() + ", it does not exist"); + continue; + } + MinecraftServer server = serverRepository.lookup(optionalPlayerStatusCache.get().getServer()).orElse(null); + if (server == null) + continue; + String header = MiscUtils.arrayToString(proxy.getProxyData().getTablist().getHeader() + .replace("{n}", "\n") + .replace("{server}", server.getName()) + .replace("{ping}", String.valueOf(player.getPing())) + .replace("{online}", String.valueOf(server.getOnline())) + .replace("{gonline}", String.valueOf(online)) + .replace("{gonlinesub}", (online == 1 ? "" : "s")) + .replace("{playername}", player.getName()) + .replace("{ip}", String.valueOf(player.getPendingConnection().getVirtualHost().getHostName()))); + String footer = MiscUtils.arrayToString(proxy.getProxyData().getTablist().getFooter() + .replace("{n}", "\n") + .replace("{server}", server.getName()) + .replace("{ping}", String.valueOf(player.getPing())) + .replace("{online}", String.valueOf(server.getOnline())) + .replace("{gonline}", String.valueOf(online)) + .replace("{gonlinesub}", (online == 1 ? "" : "s")) + .replace("{playername}", player.getName()) + .replace("{ip}", String.valueOf(player.getPendingConnection().getVirtualHost().getHostName()))); + player.setTabHeader(TextComponent.fromLegacyText(header), + TextComponent.fromLegacyText(footer)); + } + }, 2, 2, TimeUnit.SECONDS); + } + + @EventHandler + public void onHandshake(PreLoginEvent event) { + ProxiedPlayer player = proxy.getProxy().getPlayer(event.getConnection().getName()); + // If the player is already connected to the proxy, disallow login. + // This prevents a problem with account loading on actual servers as + // the server would load the new account for the new connection, and + // then unload the old account, therefore resulting in the player having + // an unloaded account + if (player != null) { + event.setCancelReason(TextComponent.fromLegacyText("§cYou're already connected to this server!")); + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void onServerConnect(ServerConnectEvent event) { + ServerConnectEvent.Reason reason = event.getReason(); + if (reason == ServerConnectEvent.Reason.UNKNOWN) + return; + ProxiedPlayer player = event.getPlayer(); + // If the player is joining the proxy, we wanna call the network connect Redis command + if (reason == ServerConnectEvent.Reason.JOIN_PROXY) + JedisCommandHandler.getInstance().send(new NetworkConnectCommand(player.getUniqueId(), player.getName(), System.currentTimeMillis())); + ServerInfo target = event.getTarget(); + if (target == null) + return; + // If the player is connecting to a server with the same name as the default server group, return + if (target.getName().equalsIgnoreCase(proxy.getDefaultServer().getName())) + return; + if (cacheRepository == null) + return; + PlayerStatusCache statusCache = cacheRepository.lookup(PlayerStatusCache.class, player.getUniqueId()).orElse(null); + if (statusCache == null) // If the player's status isn't cached, then create the cache object + statusCache = new PlayerStatusCache(player.getUniqueId(), player.getName(), target.getName(), "", System.currentTimeMillis()); + else { // If the player's status is cached, update the player's name and location + statusCache.setPlayerName(player.getName()); + statusCache.setServer(target.getName()); + } + cacheRepository.post(statusCache); // Publish the status cache object to Redis + System.out.println("Posted new player status for " + player.getName() + " (" + statusCache.toString() + ")"); + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void onDisconnect(PlayerDisconnectEvent event) { + ProxiedPlayer player = event.getPlayer(); + JedisCommandHandler.getInstance().send(new NetworkDisconnectCommand(player.getUniqueId(), player.getName(), System.currentTimeMillis())); + if (cacheRepository == null) + return; + Optional optionalPlayerStatusCache = cacheRepository.lookup(PlayerStatusCache.class, player.getUniqueId()); + if (optionalPlayerStatusCache.isEmpty()) { + System.err.println("Cannot remove player status for " + player.getName() + ", it does not exist"); + return; + } + PlayerStatusCache statusCache = optionalPlayerStatusCache.get(); + System.out.println("Removing player status for " + player.getName() + " (" + statusCache.toString() + ")"); + cacheRepository.remove(statusCache); + } +} \ No newline at end of file diff --git a/proxy/src/main/resources/bungee.yml b/proxy/src/main/resources/bungee.yml new file mode 100644 index 0000000..65b9b98 --- /dev/null +++ b/proxy/src/main/resources/bungee.yml @@ -0,0 +1,4 @@ +name: Proxy +version: 1.0-SNAPSHOT +main: zone.themcgamer.proxy.Proxy +author: MGZ Development Team \ No newline at end of file diff --git a/scripts/createServer.sh b/scripts/createServer.sh new file mode 100644 index 0000000..7efa43b --- /dev/null +++ b/scripts/createServer.sh @@ -0,0 +1,56 @@ +serverGroup=$1 +serverId=$2 +serverJarFile=$3 +templateName=$4 +pluginName=$5 +worldPath=$6 +port=$7 +startupScript=$8 + +mainDirectory=/home/minecraft +groupPath=$mainDirectory/servers/$serverGroup +serverPath=$groupPath/$serverId + +# Create the group directory if the directory doesn't exist +mkdir -p "$groupPath" + +# If the server directory already exists, delete it +if [ -d "$serverPath" ]; then + rm -r -f "$serverPath" +fi + +# Create the server directory +mkdir "$serverPath" +cd "$serverPath" || exit + +# Copying the server jar file to the server +cp $mainDirectory/upload/jars/"$serverJarFile" "$serverPath"/server.jar + +# Copying the server template to the server, unzipping it, and then removing the zip +cp $mainDirectory/upload/templates/"$templateName" template.zip +unzip template.zip +rm -r template.zip + +# Copying the plugin into the server's plugin directory +cp $mainDirectory/upload/jars/"$pluginName" plugins/"$pluginName" + +# Copying the world +mkdir world +cp $mainDirectory/upload/maps/"$worldPath" world/world.zip +cd world/ || exit +unzip world.zip +rm -r world.zip +cd .. + +# Accepting the eula +touch eula.txt +echo "eula=true" >> eula.txt + +# Writing the port to the server.properties file +touch server.properties +echo "server-ip=0.0.0.0" >> server.properties +echo "server-port=$port" >> server.properties + +# Starting the server +screen -dmS minecraftServer-"$serverId" +screen -S minecraftServer-"$serverId" -X exec "$startupScript" \ No newline at end of file diff --git a/scripts/stopServer.sh b/scripts/stopServer.sh new file mode 100644 index 0000000..4950687 --- /dev/null +++ b/scripts/stopServer.sh @@ -0,0 +1,14 @@ +serverGroup=$1 +serverId=$2 + +logsDirectory=/home/minecraft/serverLogs/$serverGroup + +# Create the group logs directory if the directory doesn't exist +mkdir -p "$logsDirectory" + +cp /home/minecraft/servers/$serverGroup/$serverId/logs/latest.log "$logsDirectory"/"$serverId-$(date +"%Y_%m_%d_%I_%M_%p").log" + +screen -S minecraftServer-"$serverId" -X kill +screen -S minecraftServer-"$serverId" -X quit + +rm -r "$serverDirectory" \ No newline at end of file diff --git a/servercontroller/build.gradle.kts b/servercontroller/build.gradle.kts new file mode 100644 index 0000000..fc64864 --- /dev/null +++ b/servercontroller/build.gradle.kts @@ -0,0 +1,29 @@ +repositories { + jcenter() +} + +dependencies { + api(project(":serverdata")) + implementation("com.mattmalec.Pterodactyl4J:Pterodactyl4J:2.BETA_24") + implementation("com.hierynomus:sshj:0.30.0") +} + +val jar by tasks.getting(Jar::class) { + manifest { + attributes["Main-Class"] = "zone.themcgamer.controller.ServerController" + } +} + +tasks { + processResources { + val tokens = mapOf("version" to project.version) + from(sourceSets["main"].resources.srcDirs) { + filter("tokens" to tokens) + } + } + + shadowJar { + archiveFileName.set("${project.rootProject.name}-${project.name}-v${project.version}.jar") + destinationDir = file("$rootDir/output") + } +} \ No newline at end of file diff --git a/servercontroller/src/main/java/zone/themcgamer/controller/ProcessRunner.java b/servercontroller/src/main/java/zone/themcgamer/controller/ProcessRunner.java new file mode 100644 index 0000000..3a19b1f --- /dev/null +++ b/servercontroller/src/main/java/zone/themcgamer/controller/ProcessRunner.java @@ -0,0 +1,63 @@ +package zone.themcgamer.controller; + +import lombok.Getter; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.util.Arrays; +import java.util.function.Consumer; + +/** + * @author Braydon + */ +public class ProcessRunner extends Thread { + private final ProcessBuilder builder; + private Process process; + private Consumer callback; + + @Getter private boolean done; + private boolean error; + + public ProcessRunner(String[] args) { + super("ProcessRunner - " + Arrays.toString(args)); + builder = new ProcessBuilder(args); + } + + @Override + public void run() { + try { + process = builder.start(); + process.waitFor(); + + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); + String line = reader.readLine(); + while (line != null) { + if (line.equals("255")) + error = true; + line = reader.readLine(); + } + } catch (Exception ex) { + ex.printStackTrace(); + } finally { + done = true; + if (callback != null) + callback.accept(error); + } + } + + public void start(Consumer callback) { + super.start(); + this.callback = callback; + } + + public int exitValue() { + if (process == null) + throw new IllegalStateException("Process was not started!"); + return process.exitValue(); + } + + public void abort() { + if (!isDone()) + process.destroy(); + } +} \ No newline at end of file diff --git a/servercontroller/src/main/java/zone/themcgamer/controller/ServerController.java b/servercontroller/src/main/java/zone/themcgamer/controller/ServerController.java new file mode 100644 index 0000000..de4a6c1 --- /dev/null +++ b/servercontroller/src/main/java/zone/themcgamer/controller/ServerController.java @@ -0,0 +1,565 @@ +package zone.themcgamer.controller; + +import lombok.SneakyThrows; +import org.apache.commons.io.FileUtils; +import zone.themcgamer.common.BuildData; +import zone.themcgamer.data.jedis.JedisController; +import zone.themcgamer.data.jedis.data.Node; +import zone.themcgamer.data.jedis.data.ServerGroup; +import zone.themcgamer.data.jedis.data.server.MinecraftServer; +import zone.themcgamer.data.jedis.data.server.ServerState; +import zone.themcgamer.data.jedis.repository.RedisRepository; +import zone.themcgamer.data.jedis.repository.impl.MinecraftServerRepository; +import zone.themcgamer.data.jedis.repository.impl.NodeRepository; +import zone.themcgamer.data.jedis.repository.impl.ServerGroupRepository; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.InetAddress; +import java.net.URL; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.*; +import java.util.stream.Collectors; + +/** + * @author Braydon + */ +public class ServerController { + private static Logger logger; + + // Repositories + private static NodeRepository nodeRepository; + private static ServerGroupRepository serverGroupRepository; + private static MinecraftServerRepository minecraftServerRepository; + + private static Node node; + private static final Set processes = new HashSet<>(); + + // Servers + private static final Set starting = new HashSet<>(); + private static final Set lagging = new HashSet<>(); + + private static ServerGroupCreator groupCreator; + + public static void main(String[] args) { + long started = System.currentTimeMillis(); + File logsDirectory = new File("logs"); + if (!logsDirectory.exists()) + logsDirectory.mkdirs(); + logger = new Logger("Server Controller", null) {{ + setLevel(Level.ALL); + }}; + // Setting up the logger + try { + String dateString = "MM-dd-yyyy HH:mm:ss"; + // The Windows operating system cannot have ":" in file names, so we need to replace + // the character with something that will work + if (System.getProperty("os.name").startsWith("Windows")) + dateString = dateString.replace(":", "-"); + SimpleDateFormat dateFormat = new SimpleDateFormat(dateString); + SimpleFormatter formatter = new SimpleFormatter() { + @Override + public synchronized String format(LogRecord record) { + return String.format( + "[%s | %s] %s", + dateFormat.format(new Date(record.getMillis())), + record.getLevel().getLocalizedName(), + record.getMessage() + ) + "\n"; + } + }; + ConsoleHandler consoleHandler = new ConsoleHandler(); + consoleHandler.setFormatter(formatter); + FileHandler fileHandler = new FileHandler("logs" + File.separator + dateFormat.format(new Date()) + ".log"); + fileHandler.setFormatter(formatter); + logger.addHandler(consoleHandler); + logger.addHandler(fileHandler); + } catch (IOException ex) { + ex.printStackTrace(); + } + new JedisController().start(); // Initializing Redis + + // Setting the repository fields + RedisRepository.getRepository(NodeRepository.class).ifPresent(nodeRepository -> ServerController.nodeRepository = nodeRepository); + RedisRepository.getRepository(ServerGroupRepository.class).ifPresent(groupRepository -> serverGroupRepository = groupRepository); + RedisRepository.getRepository(MinecraftServerRepository.class).ifPresent(minecraftServerRepository -> ServerController.minecraftServerRepository = minecraftServerRepository); + + // Setting the node and posting it to Redis + try { + URL url = new URL("http://checkip.amazonaws.com"); + try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(url.openStream()))) { + node = new Node(InetAddress.getLocalHost().getHostName(), bufferedReader.readLine(), "25665-25765"); + } + if (node == null || (node.getName().trim().isEmpty() || node.getAddress().trim().isEmpty())) { + System.err.println("Cannot resolve Node"); + System.exit(1); + return; + } + nodeRepository.post(node); + } catch (IOException ex) { + ex.printStackTrace(); + } + + // Starting the controller thread + new ControllerThread().start(); + + // This is needed for Pterodactyl so the controller is marked as started + System.out.println("Done (" + (System.currentTimeMillis() - started) + "ms)! For help, type \"help\" or \"?\"\n"); + + Scanner scanner = new Scanner(System.in); + while (scanner.hasNextLine()) { + String line = scanner.nextLine(); + + if (groupCreator != null) { + if (line.equalsIgnoreCase("exit")) { + groupCreator = null; + logger.info("Exited the group creator"); + } else { + if (groupCreator.getName() == null) { + groupCreator.setName(line); + logger.info("Alright, we'll call your server group \"" + groupCreator.getName() + "\""); + logger.info("Now, how much memory should each server have?"); + } else if (groupCreator.getMemoryPerServer() == -1L) { + long memoryPerServer = -1; + try { + memoryPerServer = Long.parseLong(line); + } catch (NumberFormatException ignored) {} + if (memoryPerServer < 512) { + logger.warning("Invalid memory amount, each server must have 512mb of ram or more"); + continue; + } + groupCreator.setMemoryPerServer(memoryPerServer); + logger.info("Okay, each server under your server group will have " + memoryPerServer + " of ram"); + logger.info("What is the path of the template you would like the server group to use?"); + } else if (groupCreator.getTemplatePath() == null) { + groupCreator.setTemplatePath(line); + logger.info("Okay, the container path will be \"" + groupCreator.getTemplatePath() + "\""); + logger.info("What would you like the plugin jar name to be?"); + } else if (groupCreator.getPluginJarName() == null) { + groupCreator.setPluginJarName(line); + logger.info("Okay, the plugin jar name will be \"" + groupCreator.getPluginJarName() + "\""); + logger.info("What would you like the world path to be?"); + } else if (groupCreator.getWorldPath() == null) { + groupCreator.setWorldPath(line); + logger.info("Okay, the world path will be \"" + groupCreator.getWorldPath() + "\""); + logger.info("What would you like the startup script to be?"); + } else if (groupCreator.getStartupScript() == null) { + groupCreator.setStartupScript(line); + logger.info("Okay, the startup script will be \"" + groupCreator.getStartupScript() + "\""); + logger.info("Almost done, what would you like the private address to be?"); + } else if (groupCreator.getPrivateAddress() == null) { + groupCreator.setPrivateAddress(line); + logger.info("Okay, the private address will be \"" + groupCreator.getPrivateAddress() + "\""); + logger.info("Alright, last thing! Would you like your server group to be static?"); + } else { + boolean staticGroup = Boolean.parseBoolean(line); + groupCreator.setStaticGroup(staticGroup); + + logger.info("All done, your server group is being built..."); + ServerGroup serverGroup = groupCreator.build(); + serverGroupRepository.post(serverGroup); + logger.info("Server group created: " + serverGroup.toString()); + groupCreator = null; + } + } + continue; + } + + switch (line.toLowerCase()) { + case "build": { + System.out.println("Build = " + BuildData.getBuild().toString()); + break; + } + case "stats": { + List serverGroups = serverGroupRepository.getCached(); + + System.out.println("Server Groups = " + serverGroups.size() + ":"); + for (ServerGroup serverGroup : serverGroups) + System.out.println(" " + serverGroup.toString()); + + System.out.println("--------------------------"); + + List minecraftServers = minecraftServerRepository.getCached(); + + System.out.println("Minecraft Servers = " + minecraftServers.size() + ":"); + int online = 0; + for (MinecraftServer minecraftServer : minecraftServers) { + online+= minecraftServer.getOnline(); + System.out.println(" " + minecraftServer.toString()); + } + + System.out.println("Online players = " + online); + break; + } + case "creategroup": { + groupCreator = new ServerGroupCreator(); + logger.info("Hi there, welcome to the server group creator! What would you like your server group to be named?"); + break; + } + } + } + } + + private static class ControllerThread extends Thread { + public ControllerThread() { + super("Server Controller Thread"); + logger.info("Started thread \"" + getName() + "\""); + } + + @Override @SneakyThrows + public void run() { + while (isAlive()) { + // This should never happen + if (node == null) { + logger.severe("Sleeping thread, the node is null..."); + Thread.sleep(1000L); + continue; + } + // Removing started servers from the starting list + for (MinecraftServer minecraftServer : minecraftServerRepository.getCached()) { + if (!minecraftServer.isRunning()) + continue; + starting.remove(minecraftServer); + } + + // Stopping Slow Servers. A server would be considered "slow" if it is in the STARTING + // state for 30 seconds or longer + AtomicInteger slowStopped = new AtomicInteger(); + starting.removeIf(server -> { + if (server.getGroup().isStaticGroup()) + return false; + if (server.getState() == ServerState.STARTING + && (System.currentTimeMillis() - server.getLastStateChange()) >= TimeUnit.SECONDS.toMillis(30L)) { + slowStopped.incrementAndGet(); + stopServer(server, StopCause.SLOW_STARTUP); + return true; + } + return false; + }); + if (slowStopped.get() > 0) { + int amount = slowStopped.get(); + logger.info("Stopped " + amount + " server" + (amount == 1 ? "" : "s") + " because they are taking too long to start"); + Thread.sleep(750L); + continue; + } + + // Stopping Dead Servers + int deadStopped = 0; + for (MinecraftServer server : new ArrayList<>(minecraftServerRepository.getCached())) { + if (server.isDead() && !server.getGroup().isStaticGroup() && (server.getNode() == null || (server.getNode().equals(node)))) { + deadStopped++; + stopServer(server, StopCause.DEAD); + } + } + if (deadStopped > 0) { + logger.info("Stopped " + deadStopped + " server" + (deadStopped == 1 ? "" : "s") + " because they didn't send a heartbeat"); + Thread.sleep(750L); + continue; + } + + // Removing Extra Directories + int extraDirectoriesRemoved = 0; + for (ServerGroup serverGroup : serverGroupRepository.getCached()) { + File serversDirectory = new File(File.separator + "home" + File.separator + "minecraft" + File.separator + "servers" + File.separator + serverGroup.getName()); + if (!serversDirectory.exists()) + continue; + File[] files = serversDirectory.listFiles(); + if (files == null) + continue; + for (File directory : files) { + if (!directory.isDirectory()) + continue; + String serverId = directory.getName(); + Optional optionalMinecraftServer = minecraftServerRepository.lookup(serverId); + if (optionalMinecraftServer.isPresent()) + continue; + extraDirectoriesRemoved++; + FileUtils.deleteQuietly(directory); + Thread.sleep(350L); + } + } + if (extraDirectoriesRemoved > 0) { + logger.info("Removed " + extraDirectoriesRemoved + " extra " + (extraDirectoriesRemoved == 1 ? "directory" : "directories")); + Thread.sleep(750L); + continue; + } + + // Stopping Shutting Down Servers + int shuttingDownStopped = 0; + for (MinecraftServer server : new ArrayList<>(minecraftServerRepository.getCached())) { + if (server.getGroup().isStaticGroup() || (server.getNode() != null && (!server.getNode().equals(node)))) + continue; + if (server.getState().isShuttingDownState()) { + shuttingDownStopped++; + stopServer(server, StopCause.SHUTDOWN); + } + } + if (shuttingDownStopped > 0) { + logger.info("Stopped " + shuttingDownStopped + " server" + (shuttingDownStopped == 1 ? "" : "s") + " because they were shutdown"); + Thread.sleep(750L); + continue; + } + + // Stopping Laggy Servers + int laggyStopped = 0; + for (MinecraftServer server : minecraftServerRepository.getCached()) { + if ((server.getNode() != null && (!server.getNode().equals(node)) || server.getGroup().isStaticGroup() || !server.isRunning())) + continue; + if (!server.isLagging()) { + lagging.remove(server); + continue; + } + if (lagging.contains(server)) { + laggyStopped++; + stopServer(server, StopCause.LAGGY); + } else { + lagging.add(server); + logger.info("Server \"" + server.getId() + "\" is lagging, it will be stopped if it continues to lag"); + } + } + if (laggyStopped > 0) { + logger.info("Stopped " + laggyStopped + " server" + (laggyStopped == 1 ? "" : "s") + " because they were lagging"); + Thread.sleep(750L); + continue; + } + + // Stopping Extra Servers + int extraStopped = 0; + for (ServerGroup group : serverGroupRepository.getCached()) { + int runningServers = Math.toIntExact(group.getServers().stream().filter(MinecraftServer::isRunning).count()); + int serversToStop = runningServers - group.getMaxServers(); + if (serversToStop <= 0) + continue; + logger.info("Attempting to stop " + serversToStop + " extra server" + (serversToStop == 1 ? "" : "s") + " for server group '" + group.getName() + "'"); + while (serversToStop > 0) { + List servers = group.getServers().stream() + .filter(minecraftServer -> minecraftServer.isRunning() && minecraftServer.getNode().equals(node)) + .sorted((a, b) -> Integer.compare(b.getNumericId(), a.getNumericId())) + .collect(Collectors.toList()); + if (servers.isEmpty()) { + logger.warning("Attempted to close an extra server but found none to close"); + break; + } + stopServer(servers.get(0), StopCause.OVER_LIMIT); + extraStopped++; + serversToStop--; + Thread.sleep(350L); + } + } + if (extraStopped > 0) { + logger.info("Stopped " + extraStopped + " extra server" + (extraStopped == 1 ? "" : "s") + " because they were over the group limit"); + Thread.sleep(750L); + continue; + } + + // Creating Servers + for (ServerGroup group : serverGroupRepository.getCached()) { + if (group.isStaticGroup()) + continue; + List starting = ServerController.starting.stream() + .filter(server -> server.getGroup().equals(group)) + .collect(Collectors.toList()); + int runningServers = Math.toIntExact(group.getServers().stream().filter(MinecraftServer::isRunning).count()); + int startingCount = starting.size(); + if (runningServers + startingCount >= group.getMaxServers()) + continue; + int serversToCreate = Math.max(Math.min(group.getMinServers(), group.getMaxServers()) - (runningServers + startingCount), 0); + serversToCreate+= group.getServers().stream().filter(minecraftServer -> minecraftServer.getOnline() >= group.getMinPlayers()).count(); + + while ((runningServers + startingCount) + serversToCreate > group.getMaxServers()) + serversToCreate--; + + if (serversToCreate <= 0) + continue; + logger.info("Attempting to create " + serversToCreate + " server" + (serversToCreate == 1 ? "" : "s") + " for server group '" + group.getName() + "'"); + + List usedIds = new ArrayList<>(); + List usedNumericIds = new ArrayList<>(); + Set usedPorts = new HashSet<>(); + + while (serversToCreate > 0) { + serversToCreate--; + + List servers = new ArrayList<>(group.getServers()); + servers.addAll(starting); + + for (MinecraftServer server : servers) { + usedIds.add(server.getId()); + if (server.isRunning()) + usedNumericIds.add(server.getNumericId()); + } + for (MinecraftServer server : servers) + usedPorts.add(server.getPort()); + + long port = node.getNextAvailablePort(usedPorts); + if (port == -1) + continue; + + String idString = ""; + while (idString.trim().isEmpty() || usedIds.contains(idString)) { + idString = group.getName().toLowerCase(); + idString+= generateID(true, false); + idString+= generateID(false, true); + } + + int numericId = 1; + while (usedNumericIds.contains(numericId)) + numericId++; + + MinecraftServer server = new MinecraftServer( + idString, + numericId, + group.getName() + "-" + numericId, + node, + group, + node.getAddress(), + port, + 0, + 0, + ServerState.STARTING, + System.currentTimeMillis(), + 0, + 0, + 20D, + group.getHost(), + group.getGame(), + "", + System.currentTimeMillis(), + -1 + ); + startingCount++; + usedIds.add(idString); + usedNumericIds.add(numericId); + usedPorts.add(port); + ServerController.starting.add(server); + minecraftServerRepository.post(server); + + ProcessRunner processRunner = new ProcessRunner(new String[] { "/bin/sh", "/home/minecraft/createServer.sh", + server.getGroup().getName(), + server.getId(), + group.getServerJar(), + group.getTemplatePath(), + group.getPluginJarName(), + group.getWorldPath(), + "" + port, + group.getStartupScript() + .replace("{{SERVER_MEMORY}}", "" + group.getMemoryPerServer()) + .replace("{{SERVER_JARFILE}}", "server.jar") + }); + processRunner.start(error -> { + if (error) + logger.severe("Failed creating Minecraft server '" + server.getId() + "'@'" + server.getAddress() + "' (" + server.getName() + ") on port " + port); + else { + logger.info("Created Minecraft server '" + server.getId() + "'@'" + server.getAddress() + "' (" + server.getName() + ") on port " + port); + } + }); + processRunner.join(100L); + if (!processRunner.isDone()) + processes.add(processRunner); + } + } + while (!processes.isEmpty()) { + processes.removeIf(process -> { + try { + process.join(100L); + } catch (InterruptedException ex) { + ex.printStackTrace(); + } + return process.isDone(); + }); + if (!processes.isEmpty()) { + logger.info("Sleeping... waiting for " + processes.size() + " processes"); + Thread.sleep(3000L); + } + } + Thread.sleep(500L); + } + } + } + + /** + * Generate a unique id based on the given values + * @param includeCapitalLetters Whether or not to include capital letters in the unique id + * @param includeLowercaseLetters Whether or not to include lowercase letters in the unique id + * @return the unique id + */ + private static String generateID(boolean includeCapitalLetters, boolean includeLowercaseLetters) { + String alphaNumericString = ""; + if (!includeCapitalLetters && !includeLowercaseLetters) + includeCapitalLetters = includeLowercaseLetters = true; + if (includeCapitalLetters) + alphaNumericString+= "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + if (includeLowercaseLetters) + alphaNumericString+= "abcdefghijklmnopqrstuvxyz"; + StringBuilder builder = new StringBuilder(2); + for (int i = 0; i < 2; i++) { + int index = (int) (alphaNumericString.length() * Math.random()); + builder.append(alphaNumericString.charAt(index)); + } + return builder.toString(); + } + + /** + * Stop the given {@link MinecraftServer} with the given {@link StopCause} + * @param server The Minecraft server to stop + * @param cause The cause to stop the server + */ + @SneakyThrows + private static void stopServer(MinecraftServer server, StopCause cause) { + if (cause != StopCause.SLOW_STARTUP) + starting.remove(server); + + String reason = "N/A"; + switch (cause) { + case SLOW_STARTUP: { + reason = "Slow Startup"; + break; + } + case DEAD: { + reason = "Sent no heartbeat"; + break; + } + case SHUTDOWN: { + reason = "Shutdown (state: " + server.getState().name() + ")"; + break; + } + case LAGGY: { + reason = "Lagging (tps: " + server.getTps() + ")"; + break; + } + case OVER_LIMIT: { + reason = "Over limit (max: " + server.getGroup().getMaxServers() + ")"; + break; + } + } + String finalReason = reason; + + // Calling the stopServer.sh script + ProcessRunner processRunner = new ProcessRunner(new String[] { "/bin/sh", "/home/minecraft/stopServer.sh", + server.getGroup().getName(), + server.getId() + }); + processRunner.start(error -> { + if (error) + logger.severe("Failed to stop server \"" + server.getId() + "\""); + else { + logger.info("Stopped server \"" + server.getId() + "\" (" + server.getName() + "): " + finalReason); + lagging.remove(server); + minecraftServerRepository.remove(server); + } + }); + processRunner.join(50L); + if (!processRunner.isDone()) { + processes.add(processRunner); + } + } + + private enum StopCause { + SLOW_STARTUP, DEAD, SHUTDOWN, LAGGY, OVER_LIMIT + } +} \ No newline at end of file diff --git a/servercontroller/src/main/java/zone/themcgamer/controller/ServerGroupCreator.java b/servercontroller/src/main/java/zone/themcgamer/controller/ServerGroupCreator.java new file mode 100644 index 0000000..b522c6b --- /dev/null +++ b/servercontroller/src/main/java/zone/themcgamer/controller/ServerGroupCreator.java @@ -0,0 +1,37 @@ +package zone.themcgamer.controller; + +import lombok.Getter; +import lombok.Setter; +import zone.themcgamer.data.jedis.data.ServerGroup; + +/** + * @author Braydon + */ +@Setter @Getter +public class ServerGroupCreator { + private String name; + private long memoryPerServer = -1L; + private String templatePath, pluginJarName, worldPath, startupScript, privateAddress; + private boolean staticGroup; + + public ServerGroup build() { + return new ServerGroup( + name, + memoryPerServer, + "server.jar", + templatePath, + pluginJarName, + worldPath, + startupScript, + privateAddress, + null, + "", + 1, + 20, + 1, + 1, + false, + staticGroup + ); + } +} \ No newline at end of file diff --git a/servercontroller/src/main/resources/META-INF/MANIFEST.MF b/servercontroller/src/main/resources/META-INF/MANIFEST.MF new file mode 100644 index 0000000..f99b583 --- /dev/null +++ b/servercontroller/src/main/resources/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: zone.themcgamer.controller.ServerController + diff --git a/serverdata/build.gradle.kts b/serverdata/build.gradle.kts new file mode 100644 index 0000000..1ca8eb6 --- /dev/null +++ b/serverdata/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + java +} + +group = "zone.themcgamer" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() +} + +dependencies { + api(project(":commons")) + implementation("redis.clients:jedis:3.4.1") + implementation("com.zaxxer:HikariCP:3.4.5") + implementation("mysql:mysql-connector-java:8.0.23") + testCompile("junit", "junit", "4.12") +} diff --git a/serverdata/src/main/java/zone/themcgamer/data/APIAccessLevel.java b/serverdata/src/main/java/zone/themcgamer/data/APIAccessLevel.java new file mode 100644 index 0000000..38f3583 --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/APIAccessLevel.java @@ -0,0 +1,8 @@ +package zone.themcgamer.data; + +/** + * @author Braydon + */ +public enum APIAccessLevel { + STANDARD, DEV +} \ No newline at end of file diff --git a/serverdata/src/main/java/zone/themcgamer/data/Rank.java b/serverdata/src/main/java/zone/themcgamer/data/Rank.java new file mode 100644 index 0000000..16349d3 --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/Rank.java @@ -0,0 +1,82 @@ +package zone.themcgamer.data; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; +import java.util.Optional; + +/** + * @author Braydon + */ +@AllArgsConstructor @RequiredArgsConstructor @Getter +public enum Rank { + // NOTE: It's important that the constant names are NOT changed during production as player's with the rank that + // was changed will have the default rank instead of the rank they originally had + + // Staff Ranks + OWNER("Owner", "§6", "§6§lOwner", RankCategory.STAFF), + MANAGER("Manager", "§e", "§e§lManager", RankCategory.STAFF), + DEVELOPER("Dev", "§9", "§9§lDev", RankCategory.STAFF), + JR_DEVELOPER("Jr.Dev", "§9", "§9§lJr.Dev", RankCategory.STAFF), + ADMIN("Admin", "§c", "§c§lAdmin", RankCategory.STAFF), + MODERATOR("Mod", "§2", "§2§lMod", RankCategory.STAFF), + HELPER("Helper", "§a", "§a§lHelper", RankCategory.STAFF), + + // Other + SR_BUILDER("Sr.Builder", "§3", "§3§lSr.Builder", RankCategory.OTHER), + BUILDER("Builder", "§b", "§b§lBuilder", RankCategory.OTHER), + PARTNER("Partner", "§d", "§d§lPartner", RankCategory.OTHER), + YOUTUBER("YouTuber", "§c", "§c§lYouTuber", RankCategory.OTHER), + + // Donor Ranks + ULTIMATE("Ultimate", "§5", "§5§lUltimate", RankCategory.DONATOR), + EXPERT("Expert", "§b", "§b§lExpert", RankCategory.DONATOR), + HERO("Hero", "§e", "§e§lHero", RankCategory.DONATOR), + SKILLED("Skilled", "§6", "§6§lSkilled", RankCategory.DONATOR), + GAMER("Gamer", "§2", "§2§lGamer", RankCategory.DONATOR), + + DEFAULT("None", "§7", "§7None", RankCategory.OTHER), + + // Sub Ranks + BETA("Beta", "§5", null, "§5Beta", RankCategory.SUB); + + private final String displayName, color, prefix; + private String suffix; + private RankCategory category; + + Rank(String displayName, String color, String prefix, RankCategory category) { + this(displayName, color, prefix, null, category); + } + + public String getNametag() { + String prefix = color; + if (hasPrefix() && (category == RankCategory.STAFF || category == RankCategory.OTHER) && this != DEFAULT) + prefix = this.prefix + " §7"; + return prefix; + } + + public boolean hasPrefix() { + return prefix != null; + } + + public boolean hasSuffix() { + return suffix != null; + } + + /** + * Lookup a rank by a string (constant name or display name) + * @param s the string to use to lookup + * @return the optional rank + */ + public static Optional lookup(String s) { + return Arrays.stream(values()) + .filter(rank -> rank.name().equalsIgnoreCase(s) || rank.getDisplayName().equalsIgnoreCase(s)) + .findFirst(); + } + + public enum RankCategory { + STAFF, DONATOR, SUB, OTHER + } +} \ No newline at end of file diff --git a/serverdata/src/main/java/zone/themcgamer/data/jedis/JedisConstants.java b/serverdata/src/main/java/zone/themcgamer/data/jedis/JedisConstants.java new file mode 100644 index 0000000..691d154 --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/jedis/JedisConstants.java @@ -0,0 +1,9 @@ +package zone.themcgamer.data.jedis; + +/** + * @author Braydon + */ +public class JedisConstants { + public static final String HOST = "172.18.0.1"; + public static final String AUTH = "CWhsuGvpYPhZt7ru"; +} \ No newline at end of file diff --git a/serverdata/src/main/java/zone/themcgamer/data/jedis/JedisController.java b/serverdata/src/main/java/zone/themcgamer/data/jedis/JedisController.java new file mode 100644 index 0000000..08d7272 --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/jedis/JedisController.java @@ -0,0 +1,59 @@ +package zone.themcgamer.data.jedis; + +import lombok.Getter; +import redis.clients.jedis.JedisPool; +import zone.themcgamer.data.jedis.cache.CacheRepository; +import zone.themcgamer.data.jedis.command.JedisCommandHandler; +import zone.themcgamer.data.jedis.command.impl.ServerStateChangeCommand; +import zone.themcgamer.data.jedis.data.server.MinecraftServer; +import zone.themcgamer.data.jedis.data.server.ServerState; +import zone.themcgamer.data.jedis.repository.RedisRepository; +import zone.themcgamer.data.jedis.repository.RedisRepositoryUpdateTask; +import zone.themcgamer.data.jedis.repository.impl.APIKeyRepository; +import zone.themcgamer.data.jedis.repository.impl.MinecraftServerRepository; +import zone.themcgamer.data.jedis.repository.impl.NodeRepository; +import zone.themcgamer.data.jedis.repository.impl.ServerGroupRepository; + +/** + * @author Braydon + * @implNote This class serves the purpose of connecting and initializing things that + * require redis, such as the {@link JedisCommandHandler} and {@link RedisRepository}'s + */ +@Getter +public class JedisController { + private JedisPool pool; + + private MinecraftServerRepository minecraftServerRepository; + + /** + * Start the controller. The initializing of the controller is done via a method + * so repositories that aren't added in this method can be added before Redis is + * initialized without running into problems + */ + public JedisController start() { + pool = new JedisPool(JedisConstants.HOST); // Configuring redis and connecting to the server + new JedisCommandHandler(); // Starting the command handler to handle commands over the network + + // Adding repositories + new CacheRepository(this); + new NodeRepository(this); + new ServerGroupRepository(this); + new APIKeyRepository(this); + minecraftServerRepository = new MinecraftServerRepository(this); + + JedisCommandHandler.getInstance().addListener(jedisCommand -> { + if (jedisCommand instanceof ServerStateChangeCommand) { + ServerStateChangeCommand serverStateChangeCommand = (ServerStateChangeCommand) jedisCommand; + if (!serverStateChangeCommand.getNewState().equals(ServerState.STOPPING)) + return; + MinecraftServer server = serverStateChangeCommand.getServer(); + System.out.println("Removed server: " + server.getId()); + //Only removed the cached server, do not actual delete the server from redis. This will only controller do! + minecraftServerRepository.getCached().remove(server); + } + }); + + new RedisRepositoryUpdateTask().start(); // Starting the repository update task + return this; + } +} \ No newline at end of file diff --git a/serverdata/src/main/java/zone/themcgamer/data/jedis/cache/CacheRepository.java b/serverdata/src/main/java/zone/themcgamer/data/jedis/cache/CacheRepository.java new file mode 100644 index 0000000..581f12e --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/jedis/cache/CacheRepository.java @@ -0,0 +1,113 @@ +package zone.themcgamer.data.jedis.cache; + +import redis.clients.jedis.Jedis; +import zone.themcgamer.data.jedis.JedisConstants; +import zone.themcgamer.data.jedis.JedisController; +import zone.themcgamer.data.jedis.repository.RedisRepository; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * @author Braydon + * @implNote This class serves the purpose of fetching all {@link ICacheItem}'s + * from Redis + */ +public class CacheRepository extends RedisRepository> { + public CacheRepository(JedisController controller) { + super(controller); + } + + @Override + public Optional> lookup(String name) { + return getCached().stream().filter(cacheItem -> cacheItem.getIdentifier().toString().equalsIgnoreCase(name)).findFirst(); + } + + @Override + public String getKey(ICacheItem cacheItem) { + return cacheItem.getType().getIdentifier() + ":" + cacheItem.getIdentifier().toString(); + } + + @Override + public Optional> fromMap(Map map) { + return Optional.empty(); + } + + @Override + public long getExpiration(ICacheItem cacheItem) { + return cacheItem.getType().getDuration(); + } + + @Override + public Map toMap(ICacheItem cacheItem) { + return cacheItem.toData(); + } + + @Override + protected void updateCache() { + List> cached = new ArrayList<>(); + try (Jedis jedis = getController().getPool().getResource()) { + jedis.auth(JedisConstants.AUTH); + for (ItemCacheType cacheType : ItemCacheType.values()) { + for (String key : jedis.keys(cacheType.getIdentifier() + ":*")) { + Map map = jedis.hgetAll(key); + if (map.isEmpty()) { + jedis.del(key); + continue; + } + try { + ICacheItem item = cacheType.getClazz().newInstance(); + item.fromData(key.split(":")[1], map); + cached.add(item); + } catch (Exception ex) { + System.err.println("Failed to update item with key '" + key + "':"); + ex.printStackTrace(); + } + } + } + } + this.cached = cached; + for (Consumer>> updateListener : getUpdateListeners()) + updateListener.accept(cached); + } + + public List> filter(Predicate> predicate) { + return getCached().stream().filter(predicate).collect(Collectors.toList()); + } + + /** + * Find a {@link ICacheItem} by the given {@link Class} and identifier + * @param clazz the class of the cache item to lookup + * @param identifier the identifier of the cache item + * @return the optional cache item + */ + public > Optional lookup(Class clazz, Object identifier) { + return lookup(clazz, cacheItem -> cacheItem.getIdentifier().equals(identifier)); + } + + /** + * Find a {@link ICacheItem} by the given {@link Class} and test against the {@link Predicate} + * @param clazz the class of the cache item to lookup + * @param predicate the predicate to test against + * @return the optional cache item + */ + public > Optional lookup(Class clazz, Predicate predicate) { + return getCached().stream() + .filter(cacheItem -> cacheItem.getClass().equals(clazz) && (predicate.test((T) cacheItem))) + .findFirst().map(clazz::cast); + } + + /** + * Find a {@link ICacheItem} by the given {@link Class} and identifier and remove it from Redis + * @param clazz the class of the cache item to remove + * @param identifier the identifier of the cache item + */ + public void remove(Class> clazz, Object identifier) { + lookup(clazz, identifier).ifPresent(this::remove); + } +} \ No newline at end of file diff --git a/serverdata/src/main/java/zone/themcgamer/data/jedis/cache/ICacheItem.java b/serverdata/src/main/java/zone/themcgamer/data/jedis/cache/ICacheItem.java new file mode 100644 index 0000000..7038bce --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/jedis/cache/ICacheItem.java @@ -0,0 +1,18 @@ +package zone.themcgamer.data.jedis.cache; + +import java.util.Map; + +/** + * @author Braydon + */ +public interface ICacheItem { + ItemCacheType getType(); + + T getIdentifier(); + + void fromData(String key, Map data); + + Map toData(); + + String toString(); +} \ No newline at end of file diff --git a/serverdata/src/main/java/zone/themcgamer/data/jedis/cache/ItemCacheType.java b/serverdata/src/main/java/zone/themcgamer/data/jedis/cache/ItemCacheType.java new file mode 100644 index 0000000..ff61b93 --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/jedis/cache/ItemCacheType.java @@ -0,0 +1,21 @@ +package zone.themcgamer.data.jedis.cache; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import zone.themcgamer.data.jedis.cache.impl.PlayerCache; +import zone.themcgamer.data.jedis.cache.impl.PlayerStatusCache; + +import java.util.concurrent.TimeUnit; + +/** + * @author Braydon + */ +@AllArgsConstructor @Getter +public enum ItemCacheType { + PLAYER("player", TimeUnit.HOURS.toMillis(24L), PlayerCache.class), + PLAYER_STATUS("playerStatus", -1L, PlayerStatusCache.class); + + private final String identifier; + private final long duration; + private final Class> clazz; +} \ No newline at end of file diff --git a/serverdata/src/main/java/zone/themcgamer/data/jedis/cache/impl/PlayerCache.java b/serverdata/src/main/java/zone/themcgamer/data/jedis/cache/impl/PlayerCache.java new file mode 100644 index 0000000..f45c78a --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/jedis/cache/impl/PlayerCache.java @@ -0,0 +1,59 @@ +package zone.themcgamer.data.jedis.cache.impl; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import zone.themcgamer.data.jedis.cache.ICacheItem; +import zone.themcgamer.data.jedis.cache.ItemCacheType; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * @author Braydon + * @implNote This cache object is stored for each player in Redis when a connection is made to the network. + * This object stores things such as the player's name, and MySQL account id for quick lookups + * and queries + */ +@AllArgsConstructor @Setter @NoArgsConstructor @Getter +public class PlayerCache implements ICacheItem { + private UUID uuid; + private String name; + private int accountId; + + @Override + public ItemCacheType getType() { + return ItemCacheType.PLAYER; + } + + @Override + public UUID getIdentifier() { + return uuid; + } + + @Override + public void fromData(String key, Map data) { + uuid = UUID.fromString(key); + name = data.get("name"); + accountId = Integer.parseInt(data.get("accountId")); + } + + @Override + public Map toData() { + Map data = new HashMap<>(); + data.put("name", name); + data.put("accountId", accountId); + return data; + } + + @Override + public String toString() { + return "PlayerCache{" + + "uuid=" + uuid + + ", name='" + name + '\'' + + ", accountId=" + accountId + + '}'; + } +} \ No newline at end of file diff --git a/serverdata/src/main/java/zone/themcgamer/data/jedis/cache/impl/PlayerStatusCache.java b/serverdata/src/main/java/zone/themcgamer/data/jedis/cache/impl/PlayerStatusCache.java new file mode 100644 index 0000000..c72a4b9 --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/jedis/cache/impl/PlayerStatusCache.java @@ -0,0 +1,66 @@ +package zone.themcgamer.data.jedis.cache.impl; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import zone.themcgamer.data.jedis.cache.ICacheItem; +import zone.themcgamer.data.jedis.cache.ItemCacheType; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * @author Braydon + * @implNote This cache object is stored for each player in Redis when a connection is made to the network. + * Unlike {@link PlayerCache}, this object is removed when a player disconnects from the network. + * The purpose of this object is to easily get a player's name, the server they're on, and the + * time they connected to the network + */ +@AllArgsConstructor @Setter @NoArgsConstructor @Getter +public class PlayerStatusCache implements ICacheItem { + private UUID uuid; + private String playerName, server, lastReply; + private long timeJoined; + + @Override + public ItemCacheType getType() { + return ItemCacheType.PLAYER_STATUS; + } + + @Override + public UUID getIdentifier() { + return uuid; + } + + @Override + public void fromData(String key, Map data) { + uuid = UUID.fromString(key); + playerName = data.get("playerName"); + server = data.get("server"); + lastReply = data.get("lastReply"); + timeJoined = Long.parseLong(data.get("timeJoined")); + } + + @Override + public Map toData() { + Map data = new HashMap<>(); + data.put("playerName", playerName); + data.put("server", server); + data.put("lastReply", lastReply); + data.put("timeJoined", timeJoined); + return data; + } + + @Override + public String toString() { + return "PlayerStatusCache{" + + "uuid=" + uuid + + ", playerName='" + playerName + '\'' + + ", server='" + server + '\'' + + ", lastReply='" + lastReply + '\'' + + ", timeJoined=" + timeJoined + + '}'; + } +} \ No newline at end of file diff --git a/serverdata/src/main/java/zone/themcgamer/data/jedis/command/JedisCommand.java b/serverdata/src/main/java/zone/themcgamer/data/jedis/command/JedisCommand.java new file mode 100644 index 0000000..18a6698 --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/jedis/command/JedisCommand.java @@ -0,0 +1,12 @@ +package zone.themcgamer.data.jedis.command; + +import lombok.Getter; +import lombok.Setter; + +/** + * @author Braydon + */ +@Setter @Getter +public class JedisCommand { + private long timeSent; +} \ No newline at end of file diff --git a/serverdata/src/main/java/zone/themcgamer/data/jedis/command/JedisCommandHandler.java b/serverdata/src/main/java/zone/themcgamer/data/jedis/command/JedisCommandHandler.java new file mode 100644 index 0000000..7eeaccf --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/jedis/command/JedisCommandHandler.java @@ -0,0 +1,84 @@ +package zone.themcgamer.data.jedis.command; + +import com.google.gson.Gson; +import lombok.Getter; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPubSub; +import zone.themcgamer.data.jedis.JedisConstants; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +/** + * @author Braydon + */ +public class JedisCommandHandler { + @Getter private static JedisCommandHandler instance; + private static final Gson GSON = new Gson(); + private static final boolean DEBUGGING = false; + + private final JedisPool pool; + + /** + * A list of listeners that are called when a {@link JedisCommand} is received + */ + private final List> listeners = new ArrayList<>(); + + /** + * Starts a new thread to handle incoming commands over the network + */ + public JedisCommandHandler() { + instance = this; + pool = new JedisPool(JedisConstants.HOST); + new Thread(() -> { + try (Jedis jedis = pool.getResource()) { + jedis.auth(JedisConstants.AUTH); + jedis.psubscribe(new JedisPubSub() { + @Override + public void onPMessage(String pattern, String channel, String message) { + if (DEBUGGING) + System.out.println("Received Redis command on channel \"" + channel + "\" with message \"" + message + "\""); + try { + String commandName = channel.split(":")[2]; + Class clazz = Class.forName(commandName); + Object commandObject = GSON.fromJson(message, clazz); + if (!(commandObject instanceof JedisCommand)) + return; + for (Consumer listener : listeners) + listener.accept((JedisCommand) commandObject); + } catch (ClassNotFoundException ignored) { + // This is ignored as all servers on the network may not be up-to-date and will not have the same + // command classes + } + } + }, "mcGamerZone:commands:*"); + } + }, "Jedis Command Thread").start(); + } + + /** + * Add a command listener + * @param consumer The listener to add + */ + public void addListener(Consumer consumer) { + listeners.add(consumer); + } + + /** + * Send a {@link JedisCommand} across the network + * @param command The command to send + */ + public void send(JedisCommand command) { + command.setTimeSent(System.currentTimeMillis()); + String className = command.getClass().getName(); + String json = GSON.toJson(command); + if (DEBUGGING) + System.out.println("Dispatching Redis command for class \"" + className + "\" with json \"" + json + "\""); + try (Jedis jedis = pool.getResource()) { + jedis.auth(JedisConstants.AUTH); + jedis.publish("mcGamerZone:commands:" + className, json); + } + } +} \ No newline at end of file diff --git a/serverdata/src/main/java/zone/themcgamer/data/jedis/command/impl/NetworkConnectCommand.java b/serverdata/src/main/java/zone/themcgamer/data/jedis/command/impl/NetworkConnectCommand.java new file mode 100644 index 0000000..20f452a --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/jedis/command/impl/NetworkConnectCommand.java @@ -0,0 +1,19 @@ +package zone.themcgamer.data.jedis.command.impl; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; +import zone.themcgamer.data.jedis.command.JedisCommand; + +import java.util.UUID; + +/** + * @author Braydon + * @implNote This command is called when a player joins the network + */ +@AllArgsConstructor @Getter @ToString +public class NetworkConnectCommand extends JedisCommand { + private final UUID uuid; + private final String name; + private final long timestamp; +} \ No newline at end of file diff --git a/serverdata/src/main/java/zone/themcgamer/data/jedis/command/impl/NetworkDisconnectCommand.java b/serverdata/src/main/java/zone/themcgamer/data/jedis/command/impl/NetworkDisconnectCommand.java new file mode 100644 index 0000000..ca79953 --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/jedis/command/impl/NetworkDisconnectCommand.java @@ -0,0 +1,19 @@ +package zone.themcgamer.data.jedis.command.impl; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; +import zone.themcgamer.data.jedis.command.JedisCommand; + +import java.util.UUID; + +/** + * @author Braydon + * @implNote This command is called when a player leaves the network + */ +@AllArgsConstructor @Getter @ToString +public class NetworkDisconnectCommand extends JedisCommand { + private final UUID uuid; + private final String name; + private final long timestamp; +} \ No newline at end of file diff --git a/serverdata/src/main/java/zone/themcgamer/data/jedis/command/impl/PlayerKickCommand.java b/serverdata/src/main/java/zone/themcgamer/data/jedis/command/impl/PlayerKickCommand.java new file mode 100644 index 0000000..8c57376 --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/jedis/command/impl/PlayerKickCommand.java @@ -0,0 +1,16 @@ +package zone.themcgamer.data.jedis.command.impl; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import zone.themcgamer.data.jedis.command.JedisCommand; + +import java.util.UUID; + +/** + * @author Braydon + */ +@AllArgsConstructor @Getter +public class PlayerKickCommand extends JedisCommand { + private final UUID uuid; + private final String reason; +} \ No newline at end of file diff --git a/serverdata/src/main/java/zone/themcgamer/data/jedis/command/impl/PlayerMessageCommand.java b/serverdata/src/main/java/zone/themcgamer/data/jedis/command/impl/PlayerMessageCommand.java new file mode 100644 index 0000000..3ee1124 --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/jedis/command/impl/PlayerMessageCommand.java @@ -0,0 +1,16 @@ +package zone.themcgamer.data.jedis.command.impl; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import zone.themcgamer.data.jedis.command.JedisCommand; + +import java.util.UUID; + +/** + * @author Braydon + */ +@AllArgsConstructor @Getter +public class PlayerMessageCommand extends JedisCommand { + private final UUID uuid; + private final String message; +} \ No newline at end of file diff --git a/serverdata/src/main/java/zone/themcgamer/data/jedis/command/impl/PunishmentsUpdateCommand.java b/serverdata/src/main/java/zone/themcgamer/data/jedis/command/impl/PunishmentsUpdateCommand.java new file mode 100644 index 0000000..d40fe71 --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/jedis/command/impl/PunishmentsUpdateCommand.java @@ -0,0 +1,16 @@ +package zone.themcgamer.data.jedis.command.impl; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import zone.themcgamer.data.jedis.command.JedisCommand; + +import java.util.UUID; + +/** + * @author Braydon + */ +@AllArgsConstructor @Getter +public class PunishmentsUpdateCommand extends JedisCommand { + private final UUID uuid; + private final String json; +} \ No newline at end of file diff --git a/serverdata/src/main/java/zone/themcgamer/data/jedis/command/impl/RankMessageCommand.java b/serverdata/src/main/java/zone/themcgamer/data/jedis/command/impl/RankMessageCommand.java new file mode 100644 index 0000000..cf50e2c --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/jedis/command/impl/RankMessageCommand.java @@ -0,0 +1,15 @@ +package zone.themcgamer.data.jedis.command.impl; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import zone.themcgamer.data.Rank; +import zone.themcgamer.data.jedis.command.JedisCommand; + +/** + * @author Braydon + */ +@AllArgsConstructor @Getter +public class RankMessageCommand extends JedisCommand { + private final Rank rank; + private final String message; +} \ No newline at end of file diff --git a/serverdata/src/main/java/zone/themcgamer/data/jedis/command/impl/ServerRestartCommand.java b/serverdata/src/main/java/zone/themcgamer/data/jedis/command/impl/ServerRestartCommand.java new file mode 100644 index 0000000..36e8178 --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/jedis/command/impl/ServerRestartCommand.java @@ -0,0 +1,13 @@ +package zone.themcgamer.data.jedis.command.impl; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import zone.themcgamer.data.jedis.command.JedisCommand; + +/** + * @author Braydon + */ +@AllArgsConstructor @Getter +public class ServerRestartCommand extends JedisCommand { + private final String serverId; +} \ No newline at end of file diff --git a/serverdata/src/main/java/zone/themcgamer/data/jedis/command/impl/ServerSendCommand.java b/serverdata/src/main/java/zone/themcgamer/data/jedis/command/impl/ServerSendCommand.java new file mode 100644 index 0000000..9cfb867 --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/jedis/command/impl/ServerSendCommand.java @@ -0,0 +1,13 @@ +package zone.themcgamer.data.jedis.command.impl; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import zone.themcgamer.data.jedis.command.JedisCommand; + +/** + * @author Braydon + */ +@AllArgsConstructor @Getter +public class ServerSendCommand extends JedisCommand { + private final String playerName, serverId; +} \ No newline at end of file diff --git a/serverdata/src/main/java/zone/themcgamer/data/jedis/command/impl/ServerStateChangeCommand.java b/serverdata/src/main/java/zone/themcgamer/data/jedis/command/impl/ServerStateChangeCommand.java new file mode 100644 index 0000000..07c1e27 --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/jedis/command/impl/ServerStateChangeCommand.java @@ -0,0 +1,16 @@ +package zone.themcgamer.data.jedis.command.impl; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import zone.themcgamer.data.jedis.command.JedisCommand; +import zone.themcgamer.data.jedis.data.server.MinecraftServer; +import zone.themcgamer.data.jedis.data.server.ServerState; + +/** + * @author Braydon + */ +@AllArgsConstructor @Getter +public class ServerStateChangeCommand extends JedisCommand { + private final MinecraftServer server; + private final ServerState oldState, newState; +} \ No newline at end of file diff --git a/serverdata/src/main/java/zone/themcgamer/data/jedis/command/impl/StaffChatCommand.java b/serverdata/src/main/java/zone/themcgamer/data/jedis/command/impl/StaffChatCommand.java new file mode 100644 index 0000000..a5326a9 --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/jedis/command/impl/StaffChatCommand.java @@ -0,0 +1,10 @@ +package zone.themcgamer.data.jedis.command.impl; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import zone.themcgamer.data.jedis.command.JedisCommand; + +@RequiredArgsConstructor @Getter +public class StaffChatCommand extends JedisCommand { + private final String prefix, username, server, message; +} \ No newline at end of file diff --git a/serverdata/src/main/java/zone/themcgamer/data/jedis/command/impl/account/AccountRankClearCommand.java b/serverdata/src/main/java/zone/themcgamer/data/jedis/command/impl/account/AccountRankClearCommand.java new file mode 100644 index 0000000..51810c6 --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/jedis/command/impl/account/AccountRankClearCommand.java @@ -0,0 +1,15 @@ +package zone.themcgamer.data.jedis.command.impl.account; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import zone.themcgamer.data.jedis.command.JedisCommand; + +import java.util.UUID; + +/** + * @author Braydon + */ +@AllArgsConstructor @Getter +public class AccountRankClearCommand extends JedisCommand { + private final UUID uuid; +} \ No newline at end of file diff --git a/serverdata/src/main/java/zone/themcgamer/data/jedis/command/impl/account/AccountRankSetCommand.java b/serverdata/src/main/java/zone/themcgamer/data/jedis/command/impl/account/AccountRankSetCommand.java new file mode 100644 index 0000000..4a62aee --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/jedis/command/impl/account/AccountRankSetCommand.java @@ -0,0 +1,16 @@ +package zone.themcgamer.data.jedis.command.impl.account; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import zone.themcgamer.data.jedis.command.JedisCommand; + +import java.util.UUID; + +/** + * @author Braydon + */ +@AllArgsConstructor @Getter +public class AccountRankSetCommand extends JedisCommand { + private final UUID uuid; + private final String constantName, rankDisplayName; +} \ No newline at end of file diff --git a/serverdata/src/main/java/zone/themcgamer/data/jedis/command/impl/player/PlayerDirectMessageEvent.java b/serverdata/src/main/java/zone/themcgamer/data/jedis/command/impl/player/PlayerDirectMessageEvent.java new file mode 100644 index 0000000..c33661c --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/jedis/command/impl/player/PlayerDirectMessageEvent.java @@ -0,0 +1,18 @@ +package zone.themcgamer.data.jedis.command.impl.player; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.Nullable; +import zone.themcgamer.data.jedis.command.JedisCommand; + +import java.util.UUID; + +/** + * Fired when a player sends a direct message to another player on the network. + */ +@RequiredArgsConstructor @Getter +public class PlayerDirectMessageEvent extends JedisCommand { + private final String senderDisplayName, message; + @Nullable private final UUID receiver; + private final boolean reply; +} diff --git a/serverdata/src/main/java/zone/themcgamer/data/jedis/data/APIKey.java b/serverdata/src/main/java/zone/themcgamer/data/jedis/data/APIKey.java new file mode 100644 index 0000000..879b567 --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/jedis/data/APIKey.java @@ -0,0 +1,14 @@ +package zone.themcgamer.data.jedis.data; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import zone.themcgamer.data.APIAccessLevel; + +/** + * @author Braydon + */ +@AllArgsConstructor @Getter +public class APIKey { + private final String key; + private final APIAccessLevel accessLevel; +} \ No newline at end of file diff --git a/serverdata/src/main/java/zone/themcgamer/data/jedis/data/Node.java b/serverdata/src/main/java/zone/themcgamer/data/jedis/data/Node.java new file mode 100644 index 0000000..020adf9 --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/jedis/data/Node.java @@ -0,0 +1,84 @@ +package zone.themcgamer.data.jedis.data; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.SneakyThrows; +import lombok.ToString; +import zone.themcgamer.data.jedis.data.server.MinecraftServer; +import zone.themcgamer.data.jedis.repository.RedisRepository; +import zone.themcgamer.data.jedis.repository.impl.MinecraftServerRepository; + +import java.net.InetAddress; +import java.util.*; +import java.util.stream.Collectors; + +/** + * @author Braydon + * @implNote A "Node" is a Dedicated Server, each node can be used to host a + * {@link MinecraftServer} + */ +@AllArgsConstructor @Getter @ToString +public class Node { + private final String name, address, portRange; + + /** + * Check whether or not the Node is reachable + * @return the reachable state + */ + @SneakyThrows + public boolean isReachable() { + return InetAddress.getByName(address).isReachable(5000); + } + + public long getNextAvailablePort(Set used) { + if (!portRange.contains("-")) + return -1; + String[] split = portRange.split("-"); + long min, max; + try { + min = Long.parseLong(split[0]); + max = Long.parseLong(split[1]); + } catch (NumberFormatException ex) { + ex.printStackTrace(); + return -1; + } + for (long port = min; port <= max; port++) { + long finalPort = port; + if (used.contains(port) || getServers().stream().anyMatch(minecraftServer -> minecraftServer.getPort() == finalPort)) + continue; + return port; + } + return -1; + } + + /** + * Get a list of {@link MinecraftServer}'s running under this node + * @return the list of servers + */ + public Collection getServers() { + Set servers = new HashSet<>(); + Optional minecraftServerRepository = RedisRepository.getRepository(MinecraftServerRepository.class); + if (!minecraftServerRepository.isPresent()) + return servers; + servers.addAll(new ArrayList<>(minecraftServerRepository.get().getCached()).parallelStream() + .filter(minecraftServer -> minecraftServer.getNode() != null && (minecraftServer.getNode().equals(this))) + .collect(Collectors.toList())); + return servers; + } + + @Override + public boolean equals(Object other) { + if (this == other) + return true; + if (other == null || getClass() != other.getClass()) + return false; + Node node = (Node) other; + return Objects.equals(name, node.name) + && Objects.equals(address, node.address); + } + + @Override + public int hashCode() { + return Objects.hash(name, address); + } +} diff --git a/serverdata/src/main/java/zone/themcgamer/data/jedis/data/ServerGroup.java b/serverdata/src/main/java/zone/themcgamer/data/jedis/data/ServerGroup.java new file mode 100644 index 0000000..00f4642 --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/jedis/data/ServerGroup.java @@ -0,0 +1,57 @@ +package zone.themcgamer.data.jedis.data; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; +import zone.themcgamer.data.jedis.data.server.MinecraftServer; +import zone.themcgamer.data.jedis.repository.RedisRepository; +import zone.themcgamer.data.jedis.repository.impl.MinecraftServerRepository; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * @author Braydon + * @implNote A server group is the owner of a {@link MinecraftServer}, it contains properties + * such as how much ram to use, which jar to use, and more + */ +@AllArgsConstructor @Getter @ToString +public class ServerGroup { + private final String name; + private final long memoryPerServer; + private final String serverJar, templatePath, pluginJarName, worldPath, startupScript, privateAddress; + private final UUID host; + private final String game; + private final int minPlayers, maxPlayers, minServers, maxServers; + private final boolean kingdom, staticGroup; + + /** + * Get a list of {@link MinecraftServer}'s running under this server group + * @return the list of servers + */ + public Collection getServers() { + Set servers = new HashSet<>(); + Optional minecraftServerRepository = RedisRepository.getRepository(MinecraftServerRepository.class); + if (!minecraftServerRepository.isPresent()) + return servers; + servers.addAll(new ArrayList<>(minecraftServerRepository.get().getCached()).parallelStream() + .filter(minecraftServer -> minecraftServer.getGroup().equals(this)) + .collect(Collectors.toList())); + return servers; + } + + @Override + public boolean equals(Object other) { + if (this == other) + return true; + if (other == null || getClass() != other.getClass()) + return false; + ServerGroup group = (ServerGroup) other; + return Objects.equals(name, group.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } +} \ No newline at end of file diff --git a/serverdata/src/main/java/zone/themcgamer/data/jedis/data/server/MinecraftServer.java b/serverdata/src/main/java/zone/themcgamer/data/jedis/data/server/MinecraftServer.java new file mode 100644 index 0000000..7b13e41 --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/jedis/data/server/MinecraftServer.java @@ -0,0 +1,118 @@ +package zone.themcgamer.data.jedis.data.server; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import zone.themcgamer.data.jedis.command.JedisCommandHandler; +import zone.themcgamer.data.jedis.command.impl.ServerStateChangeCommand; +import zone.themcgamer.data.jedis.data.Node; +import zone.themcgamer.data.jedis.data.ServerGroup; + +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +/** + * @author Braydon + */ +@AllArgsConstructor @Setter @Getter @ToString +public class MinecraftServer { + private final String id; + private final int numericId; + private final String name; + private final Node node; + private final ServerGroup group; + private final String address; + private final long port; + + private int usedRam, maxRam; + private ServerState state; + private long lastStateChange; + + private int online, maxPlayers; + private double tps; + private UUID host; + private String game; + + private String metaData; + private final long created; + private long lastHeartbeat; + + /** + * Set the state of the server to the given state + * @param state the state + */ + public void setState(ServerState state) { + JedisCommandHandler.getInstance().send(new ServerStateChangeCommand(this, this.state, state)); + this.state = state; + lastStateChange = System.currentTimeMillis(); + } + + /** + * Return whether or not the Minecraft server is running by checking + * the dead state and the server state + * @return the running state + */ + public boolean isRunning() { + if (isDead()) + return false; + return state == ServerState.RUNNING; + } + + /** + * Return whether or not the Minecraft server is lagging by + * checking if ths tps is 15 or below + * @return the lagging state + */ + public boolean isLagging() { + return tps <= 15; + } + + /** + * Return whether or not the Minecraft server is dead. + * A server is considered dead if it hasn't sent a heartbeat + * within 8 seconds and the server is older than 30 seconds + * @return the dead state + */ + public boolean isDead() { + if (isNew()) + return false; + return (System.currentTimeMillis() - lastHeartbeat) >= TimeUnit.SECONDS.toMillis(8L); + } + + /** + * Return whether or not the server was created in the last minute + * @return the new state + */ + public boolean isNew() { + return getUptime() < TimeUnit.SECONDS.toMillis(30L); + } + + /** + * Get the uptime of the server in millis + * @return the uptime + */ + public long getUptime() { + return System.currentTimeMillis() - created; + } + + @Override + public boolean equals(Object other) { + if (this == other) + return true; + if (other == null || getClass() != other.getClass()) + return false; + MinecraftServer that = (MinecraftServer) other; + return numericId == that.numericId + && Objects.equals(id, that.id) + && Objects.equals(name, that.name) + && Objects.equals(node, that.node) + && Objects.equals(group, that.group); + } + + @Override + public int hashCode() { + return Objects.hash(id, numericId, name, node, group); + } +} \ No newline at end of file diff --git a/serverdata/src/main/java/zone/themcgamer/data/jedis/data/server/ServerState.java b/serverdata/src/main/java/zone/themcgamer/data/jedis/data/server/ServerState.java new file mode 100644 index 0000000..18462fe --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/jedis/data/server/ServerState.java @@ -0,0 +1,18 @@ +package zone.themcgamer.data.jedis.data.server; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author Braydon + */ +@AllArgsConstructor @Getter +public enum ServerState { + STARTING(false), + RUNNING(false), + UPDATING(true), + RESTARTING(true), + STOPPING(true); + + private final boolean shuttingDownState; +} \ No newline at end of file diff --git a/serverdata/src/main/java/zone/themcgamer/data/jedis/repository/RedisRepository.java b/serverdata/src/main/java/zone/themcgamer/data/jedis/repository/RedisRepository.java new file mode 100644 index 0000000..c391aae --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/jedis/repository/RedisRepository.java @@ -0,0 +1,167 @@ +package zone.themcgamer.data.jedis.repository; + +import lombok.Getter; +import redis.clients.jedis.Jedis; +import zone.themcgamer.data.jedis.JedisConstants; +import zone.themcgamer.data.jedis.JedisController; + +import java.util.*; +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** + * @author Braydon + */ +@Getter +public abstract class RedisRepository { + @Getter private static final Set> repositories = new HashSet<>(); + + private final JedisController controller; + private final String pattern; + protected List cached = new ArrayList<>(); + private final Collection>> updateListeners = new ArrayList<>(); + + public RedisRepository(JedisController controller) { + this(controller, null); + } + + public RedisRepository(JedisController controller, String pattern) { + this.controller = controller; + this.pattern = pattern; + repositories.add(this); + } + + /** + * Lookup a {@link T} object by {@link I} + * @param i The object to use to do the lookup + * @return optional {@link T} + */ + public abstract Optional lookup(I i); + + /** + * Get the Redis key for the given {@link T} object + * @param t The object to get the key for + * @return the key + */ + public abstract String getKey(T t); + + /** + * Create a new {@link T} instance from the given map + * @param map The map + * @return the new instance + */ + public abstract Optional fromMap(Map map); + + /** + * Get how long a key should be stored + * @return the expiration time, -1 if none + */ + public abstract long getExpiration(T t); + + /** + * Create a new map for the given {@link T} object + * @param t The object` + * @return the map + */ + public abstract Map toMap(T t); + + /** + * Get the cached value for the repository + * @return the cached values + */ + public List getCached() { + return new ArrayList<>(cached); + } + + /** + * Lookup a {@link T} object that tests against the {@link Predicate} + * @param predicate The predicate to test + * @return optional {@link T} + */ + public Optional lookup(Predicate predicate) { + return cached.stream().filter(predicate).findFirst(); + } + + /** + * Add an update listener + * @param consumer the consumer to add + */ + public void addUpdateListener(Consumer> consumer) { + updateListeners.add(consumer); + } + + /** + * Adds the given {@link T} object to the local cache and to Redis + * @param t The object to add + */ + public void post(T t) { + cached.add(t); + String key = getKey(t); + if (key == null || (key.isEmpty())) + throw new IllegalArgumentException("Cannot post, the key is null or empty: \"" + (key == null ? "null" : key) + "\""); + Map map = new HashMap<>(); + for (Map.Entry entry : toMap(t).entrySet()) + map.put(entry.getKey(), entry.getValue().toString()); + try (Jedis jedis = controller.getPool().getResource()) { + jedis.auth(JedisConstants.AUTH); + jedis.hmset(key, map); + + long expiration = getExpiration(t); + if (expiration > 0) + jedis.pexpire(key, expiration); + } + } + + /** + * Remove the given {@link T} object from the local cache and from Redis + * @param t The object to remove + */ + public void remove(T t) { + cached.remove(t); + String key = getKey(t); + if (key == null || (key.isEmpty())) + throw new IllegalArgumentException("Cannot remove, the key is null or empty: \"" + (key == null ? "null" : key) + "\""); + try (Jedis jedis = controller.getPool().getResource()) { + jedis.auth(JedisConstants.AUTH); + jedis.del(key); + } + } + + /** + * Clear the local repository cache and fetch {@link T} from Redis and + * populate the local cache with it + */ + protected void updateCache() { + if (pattern == null) + return; + List cached = new ArrayList<>(); + try (Jedis jedis = controller.getPool().getResource()) { + jedis.auth(JedisConstants.AUTH); + Set keys = jedis.keys(pattern); + for (String key : keys) { + Map data = jedis.hgetAll(key); + if (data.isEmpty()) + continue; + try { + fromMap(jedis.hgetAll(key)).ifPresent(cached::add); + // We ignore this exception so if a repository object is being created + // in Redis and is not completed, it will skip it + } catch (Exception ignored) {} + } + } + this.cached = cached; + for (Consumer> updateListener : updateListeners) + updateListener.accept(cached); + } + + /** + * Get a {@link RedisRepository} from the given {@link Class} + * @param clazz The class to get the repository from + * @return the repository + */ + public static > Optional getRepository(Class clazz) { + return repositories.stream() + .filter(repository -> repository.getClass().equals(clazz)) + .findFirst().map(clazz::cast); + } +} \ No newline at end of file diff --git a/serverdata/src/main/java/zone/themcgamer/data/jedis/repository/RedisRepositoryUpdateTask.java b/serverdata/src/main/java/zone/themcgamer/data/jedis/repository/RedisRepositoryUpdateTask.java new file mode 100644 index 0000000..bac0109 --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/jedis/repository/RedisRepositoryUpdateTask.java @@ -0,0 +1,25 @@ +package zone.themcgamer.data.jedis.repository; + +import lombok.SneakyThrows; + +/** + * @author Braydon + */ +public class RedisRepositoryUpdateTask extends Thread { + public RedisRepositoryUpdateTask() { + super("Redis Repository Update Task"); + } + + /** + * Loop through all of the {@link RedisRepository}'s and update their local cache + */ + @Override + @SneakyThrows + public void run() { + while (isAlive()) { + for (RedisRepository repository : RedisRepository.getRepositories()) + repository.updateCache(); + Thread.sleep(30L); + } + } +} \ No newline at end of file diff --git a/serverdata/src/main/java/zone/themcgamer/data/jedis/repository/impl/APIKeyRepository.java b/serverdata/src/main/java/zone/themcgamer/data/jedis/repository/impl/APIKeyRepository.java new file mode 100644 index 0000000..785047a --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/jedis/repository/impl/APIKeyRepository.java @@ -0,0 +1,50 @@ +package zone.themcgamer.data.jedis.repository.impl; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import zone.themcgamer.data.jedis.JedisController; +import zone.themcgamer.data.jedis.data.APIKey; +import zone.themcgamer.data.jedis.repository.RedisRepository; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * @author Braydon + */ +public class APIKeyRepository extends RedisRepository> { + private static final Gson GSON = new Gson(); + + public APIKeyRepository(JedisController controller) { + super(controller, "apiKeys"); + } + + @Override + public Optional> lookup(String name) { + if (getCached().isEmpty()) + return Optional.of(Collections.emptyList()); + return Optional.of(getCached().get(0).stream().filter(apiKey -> apiKey.getKey().equals(name)).collect(Collectors.toList())); + } + + @Override + public String getKey(List apiKeys) { + return "apiKeys"; + } + + @Override + public Optional> fromMap(Map map) { + return Optional.of(GSON.fromJson(map.get("keys"), new TypeToken>() {}.getType())); + } + + @Override + public long getExpiration(List apiKeys) { + return -1; + } + + @Override + public Map toMap(List apiKeys) { + Map data = new HashMap<>(); + data.put("keys", GSON.toJson(apiKeys)); + return data; + } +} \ No newline at end of file diff --git a/serverdata/src/main/java/zone/themcgamer/data/jedis/repository/impl/MinecraftServerRepository.java b/serverdata/src/main/java/zone/themcgamer/data/jedis/repository/impl/MinecraftServerRepository.java new file mode 100644 index 0000000..c95f655 --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/jedis/repository/impl/MinecraftServerRepository.java @@ -0,0 +1,95 @@ +package zone.themcgamer.data.jedis.repository.impl; + +import zone.themcgamer.common.MiscUtils; +import zone.themcgamer.data.jedis.JedisController; +import zone.themcgamer.data.jedis.data.ServerGroup; +import zone.themcgamer.data.jedis.data.server.MinecraftServer; +import zone.themcgamer.data.jedis.data.server.ServerState; +import zone.themcgamer.data.jedis.repository.RedisRepository; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * @author Braydon + * @implNote This class serves the purpose of fetching all {@link MinecraftServer}'s + * from Redis + */ +public class MinecraftServerRepository extends RedisRepository { + public MinecraftServerRepository(JedisController controller) { + super(controller, "minecraftServer:*"); + } + + @Override + public Optional lookup(String id) { + return getCached().stream().filter(minecraftServer -> minecraftServer.getId().equalsIgnoreCase(id)).findFirst(); + } + + @Override + public String getKey(MinecraftServer minecraftServer) { + return "minecraftServer:" + minecraftServer.getId(); + } + + @Override + public Optional fromMap(Map map) { + Optional optionalNodeRepository = RedisRepository.getRepository(NodeRepository.class); + if (optionalNodeRepository.isEmpty()) + return Optional.empty(); + Optional serverGroupRepository = RedisRepository.getRepository(ServerGroupRepository.class); + if (serverGroupRepository.isEmpty()) + return Optional.empty(); + Optional optionalServerGroup = serverGroupRepository.get().lookup(map.get("group")); + return optionalServerGroup.map(serverGroup -> new MinecraftServer( + map.get("id"), + Integer.parseInt(map.get("numericId")), + map.get("name"), + optionalNodeRepository.get().lookup(map.get("node")).orElse(null), + serverGroup, + map.get("address"), + Long.parseLong(map.get("port")), + Integer.parseInt(map.get("usedRam")), + Integer.parseInt(map.get("maxRam")), + ServerState.valueOf(map.get("state")), + Long.parseLong(map.get("lastStateChange")), + Integer.parseInt(map.get("online")), + Integer.parseInt(map.get("maxPlayers")), + Double.parseDouble(map.get("tps")), + MiscUtils.getUuid(map.get("host")), + map.get("game"), + map.get("metadata"), + Long.parseLong(map.get("created")), + Long.parseLong(map.get("lastHeartbeat")) + )); + } + + @Override + public long getExpiration(MinecraftServer minecraftServer) { + return -1; + } + + @Override + public Map toMap(MinecraftServer minecraftServer) { + Map data = new HashMap<>(); + data.put("id", minecraftServer.getId()); + data.put("numericId", minecraftServer.getNumericId()); + data.put("name", minecraftServer.getName()); + data.put("node", minecraftServer.getNode() == null ? "" : minecraftServer.getNode().getName()); + data.put("group", minecraftServer.getGroup().getName()); + data.put("address", minecraftServer.getAddress()); + data.put("port", minecraftServer.getPort()); + data.put("usedRam", minecraftServer.getUsedRam()); + data.put("maxRam", minecraftServer.getMaxRam()); + data.put("state", minecraftServer.getState().name()); + data.put("lastStateChange", minecraftServer.getLastStateChange()); + data.put("online", minecraftServer.getOnline()); + data.put("maxPlayers", minecraftServer.getMaxPlayers()); + data.put("tps", minecraftServer.getTps()); + data.put("host", minecraftServer.getHost() == null ? "" : minecraftServer.getHost().toString()); + data.put("game", minecraftServer.getGame()); + data.put("metadata", minecraftServer.getMetaData()); + data.put("created", minecraftServer.getCreated()); + data.put("lastHeartbeat", minecraftServer.getLastHeartbeat()); + return data; + } +} \ No newline at end of file diff --git a/serverdata/src/main/java/zone/themcgamer/data/jedis/repository/impl/NodeRepository.java b/serverdata/src/main/java/zone/themcgamer/data/jedis/repository/impl/NodeRepository.java new file mode 100644 index 0000000..227a4ab --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/jedis/repository/impl/NodeRepository.java @@ -0,0 +1,58 @@ +package zone.themcgamer.data.jedis.repository.impl; + +import lombok.SneakyThrows; +import zone.themcgamer.data.jedis.JedisController; +import zone.themcgamer.data.jedis.data.Node; +import zone.themcgamer.data.jedis.repository.RedisRepository; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * @author Braydon + * @implNote This class serves the purpose of fetching all {@link Node}'s + * from Redis + */ +public class NodeRepository extends RedisRepository { + public NodeRepository(JedisController controller) { + super(controller, "node:*"); + } + + @Override @SneakyThrows + public Optional lookup(String name) { + while (getCached().isEmpty()) { + updateCache(); + Thread.sleep(10L); + } + return getCached().stream().filter(node -> node.getName().equalsIgnoreCase(name)).findFirst(); + } + + @Override + public String getKey(Node node) { + return "node:" + node.getName(); + } + + @Override + public Optional fromMap(Map map) { + return Optional.of(new Node( + map.get("name"), + map.get("address"), + map.get("portRange") + )); + } + + @Override + public long getExpiration(Node node) { + return -1; + } + + @Override + public Map toMap(Node node) { + Map data = new HashMap<>(); + data.put("name", node.getName()); + data.put("address", node.getAddress()); + data.put("portRange", node.getPortRange()); + return data; + } +} diff --git a/serverdata/src/main/java/zone/themcgamer/data/jedis/repository/impl/ServerGroupRepository.java b/serverdata/src/main/java/zone/themcgamer/data/jedis/repository/impl/ServerGroupRepository.java new file mode 100644 index 0000000..40dd64c --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/jedis/repository/impl/ServerGroupRepository.java @@ -0,0 +1,80 @@ +package zone.themcgamer.data.jedis.repository.impl; + +import zone.themcgamer.common.MiscUtils; +import zone.themcgamer.data.jedis.JedisController; +import zone.themcgamer.data.jedis.data.ServerGroup; +import zone.themcgamer.data.jedis.repository.RedisRepository; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * @author Braydon + * @implNote This class serves the purpose of fetching all {@link ServerGroup}'s + * from Redis + */ +public class ServerGroupRepository extends RedisRepository { + public ServerGroupRepository(JedisController controller) { + super(controller, "serverGroup:*"); + } + + @Override + public Optional lookup(String name) { + return getCached().stream().filter(group -> group.getName().equalsIgnoreCase(name)).findFirst(); + } + + @Override + public String getKey(ServerGroup serverGroup) { + return "serverGroup:" + serverGroup.getName(); + } + + @Override + public Optional fromMap(Map map) { + return Optional.of(new ServerGroup( + map.get("name"), + Long.parseLong(map.get("memoryPerServer")), + map.get("serverJar"), + map.get("templatePath"), + map.get("pluginJarName"), + map.get("worldPath"), + map.get("startupScript"), + map.get("privateAddress"), + MiscUtils.getUuid(map.get("host")), + map.get("game"), + Integer.parseInt(map.get("minPlayers")), + Integer.parseInt(map.get("maxPlayers")), + Integer.parseInt(map.get("minServers")), + Integer.parseInt(map.get("maxServers")), + Boolean.parseBoolean(map.get("kingdom")), + Boolean.parseBoolean(map.get("static")) + )); + } + + @Override + public long getExpiration(ServerGroup serverGroup) { + return -1; + } + + @Override + public Map toMap(ServerGroup serverGroup) { + Map data = new HashMap<>(); + data.put("name", serverGroup.getName()); + data.put("memoryPerServer", serverGroup.getMemoryPerServer()); + data.put("serverJar", serverGroup.getServerJar()); + data.put("templatePath", serverGroup.getTemplatePath()); + data.put("pluginJarName", serverGroup.getPluginJarName()); + data.put("worldPath", serverGroup.getWorldPath()); + data.put("startupScript", serverGroup.getStartupScript()); + data.put("privateAddress", serverGroup.getPrivateAddress()); + data.put("host", serverGroup.getHost() == null ? "" : serverGroup.getHost().toString()); + data.put("game", serverGroup.getGame()); + data.put("minPlayers", serverGroup.getMinPlayers()); + data.put("maxPlayers", serverGroup.getMaxPlayers()); + data.put("minServers", serverGroup.getMinServers()); + data.put("maxServers", serverGroup.getMaxServers()); + data.put("kingdom", serverGroup.isKingdom()); + data.put("static", serverGroup.isStaticGroup()); + return data; + } +} \ No newline at end of file diff --git a/serverdata/src/main/java/zone/themcgamer/data/mysql/MySQLConstants.java b/serverdata/src/main/java/zone/themcgamer/data/mysql/MySQLConstants.java new file mode 100644 index 0000000..83f905b --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/mysql/MySQLConstants.java @@ -0,0 +1,11 @@ +package zone.themcgamer.data.mysql; + +/** + * @author Braydon + */ +public class MySQLConstants { + public static final String HOST = "172.18.0.1"; + + public static final String USERNAME = "mcgamerzone"; + public static final String AUTH = "n3oCqfGeCG7lUcOlqvUG2JfyKMtEZakG0eNIA"; +} \ No newline at end of file diff --git a/serverdata/src/main/java/zone/themcgamer/data/mysql/MySQLController.java b/serverdata/src/main/java/zone/themcgamer/data/mysql/MySQLController.java new file mode 100644 index 0000000..5b6d22d --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/mysql/MySQLController.java @@ -0,0 +1,85 @@ +package zone.themcgamer.data.mysql; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import lombok.Getter; +import zone.themcgamer.data.mysql.data.Table; +import zone.themcgamer.data.mysql.data.column.Column; +import zone.themcgamer.data.mysql.data.column.impl.IntegerColumn; +import zone.themcgamer.data.mysql.data.column.impl.LongColumn; +import zone.themcgamer.data.mysql.data.column.impl.StringColumn; + +import java.sql.SQLException; + +/** + * @author Braydon + */ +@Getter +public class MySQLController { + private final HikariDataSource dataSource; + + public MySQLController(boolean production) { + // Connecting to the MySQL server + HikariConfig config = new HikariConfig(); + config.setJdbcUrl("jdbc:mysql://" + MySQLConstants.HOST + ":3306/" + MySQLConstants.USERNAME + "_" + (production ? "production" : "dev") + "?allowMultiQueries=true"); + config.setUsername(MySQLConstants.USERNAME); + config.setPassword(MySQLConstants.AUTH); + config.setDriverClassName("com.mysql.cj.jdbc.Driver"); + config.addDataSourceProperty("cachePrepStmts", "true"); + config.addDataSourceProperty("prepStmtCacheSize", "250"); + config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); + dataSource = new HikariDataSource(config); + + // Creating the tables + Table[] tables = new Table[] { + new Table("accounts", new Column[] { + new IntegerColumn("id", true, false), + new StringColumn("uuid", 36, false), + new StringColumn("name", 16, false), + new StringColumn("primaryRank", 25, false), + new StringColumn("secondaryRanks", 255, false), + new IntegerColumn("gold", false, false), + new IntegerColumn("gems", false, false), + new StringColumn("ipAddress", 255, false), + new LongColumn("firstLogin", false), + new LongColumn("lastLogin", false) + }, new String[] { "id" }), + + new Table("punishments", new Column[] { + new IntegerColumn("id", true, false), + new StringColumn("targetIp", 255, false), + new StringColumn("targetUuid", 36, true), + new StringColumn("category", 20, false), + new StringColumn("offense", 20, false), + new IntegerColumn("severity", false, false), + new StringColumn("staffUuid", 36, true), + new StringColumn("staffName", 16, false), + new LongColumn("timeIssued", false), + new LongColumn("duration", false), + new StringColumn("reason", 255, false), + new StringColumn("removeStaffUuid", 36, true), + new StringColumn("removeStaffName", 16, true), + new StringColumn("removeReason", 255, true), + new LongColumn("timeRemoved", false), + }, new String[] { "id" }), + + new Table("tasks", new Column[] { + new IntegerColumn("accountId", false, false), + new StringColumn("task", 255, false) + }, new String[] { "accountId" }), + + new Table("kits", new Column[] { + new IntegerColumn("accountId", false, false), + new StringColumn("game", 50, false), + new StringColumn("kit", 50, false) + }, new String[] { "accountId" }) + }; + for (Table table : tables) { + try { + table.create(this, true); + } catch (SQLException ex) { + ex.printStackTrace(); + } + } + } +} \ No newline at end of file diff --git a/serverdata/src/main/java/zone/themcgamer/data/mysql/data/Table.java b/serverdata/src/main/java/zone/themcgamer/data/mysql/data/Table.java new file mode 100644 index 0000000..eb8ad35 --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/mysql/data/Table.java @@ -0,0 +1,90 @@ +package zone.themcgamer.data.mysql.data; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import zone.themcgamer.data.mysql.MySQLController; +import zone.themcgamer.data.mysql.data.column.Column; +import zone.themcgamer.data.mysql.data.column.impl.IntegerColumn; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.concurrent.CompletableFuture; + +/** + * @author Braydon + * @implNote This class represents a table in MySQL + */ +@AllArgsConstructor @RequiredArgsConstructor @Getter +public class Table { + private final String name; + private final Column[] columns; + private String[] primaryKeys; + + /** + * Create the table in the MySQL database + * @param mySQLController the MySQL controller + * @param ignoreExisting whether or not to ignore the existing table + * @throws SQLException exception + */ + public void create(MySQLController mySQLController, boolean ignoreExisting) throws SQLException { + // Checking the auto incrementing column count + int autoIncrementingTables = 0; + for (Column column : columns) { + if (column instanceof IntegerColumn && ((IntegerColumn) column).isAutoIncrement()) { + autoIncrementingTables++; + } + } + if (autoIncrementingTables > 1) + throw new SQLException("Cannot create table with more than 1 auto incrementing table: " + autoIncrementingTables); + // Constructing the query string + StringBuilder queryBuilder = new StringBuilder("CREATE TABLE " + (ignoreExisting ? "IF NOT EXISTS " : "") + "`" + name + "` ("); + + for (Column column : columns) { + boolean autoIncrement = column instanceof IntegerColumn && ((IntegerColumn) column).isAutoIncrement(); + + queryBuilder.append("`").append(column.getName()).append("` ").append(column.getType().name()); + // If the column length is larger than 0, we wanna add (x) to the column type + if (column.getLength() > 0) + queryBuilder.append("(").append(column.getLength()).append(")"); + // If the column isn't nullable, add "NOT NULL" to the query + if (!column.isNullable()) + queryBuilder.append(" NOT NULL"); + // If the column is set to auto increment, add "AUTO_INCREMENT" to the query + if (autoIncrement) + queryBuilder.append(" AUTO_INCREMENT"); + queryBuilder.append(", "); + } + String query = queryBuilder.toString(); + query = query.substring(0, query.length() - 2); + + // Appending the primary key(s) to the query + if (primaryKeys != null && (primaryKeys.length > 0)) { + query+= ", PRIMARY KEY ("; + for (String primaryKey : primaryKeys) + query+= "`" + primaryKey + "`, "; + query = query.substring(0, query.length() - 2) + ")"; + } + query+= ");"; + + // Creating the table + String finalQuery = query; + CompletableFuture.runAsync(() -> { + Connection connection = null; + try { + connection = mySQLController.getDataSource().getConnection(); + connection.prepareStatement(finalQuery).execute(); + } catch (SQLException ex) { + ex.printStackTrace(); + } finally { + if (connection != null) { + try { + connection.close(); + } catch (SQLException ex) { + ex.printStackTrace(); + } + } + } + }); + } +} \ No newline at end of file diff --git a/serverdata/src/main/java/zone/themcgamer/data/mysql/data/column/Column.java b/serverdata/src/main/java/zone/themcgamer/data/mysql/data/column/Column.java new file mode 100644 index 0000000..1fe85a4 --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/mysql/data/column/Column.java @@ -0,0 +1,30 @@ +package zone.themcgamer.data.mysql.data.column; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import zone.themcgamer.data.mysql.data.Table; + +/** + * @author Braydon + * @implNote This class represents a column in a MySQL {@link Table} + */ +@AllArgsConstructor @RequiredArgsConstructor @Setter @Getter +public class Column { + private final String name; + private T value; + private final ColumnType type; + private final int length; + private final boolean nullable; + + /** + * Construct a new column with a name and a value. This is used when executing + * queries in a repository. + * @param name the name of the column + * @param value the value in the column + */ + public Column(String name, T value) { + this(name, value, null, -1, false); + } +} \ No newline at end of file diff --git a/serverdata/src/main/java/zone/themcgamer/data/mysql/data/column/ColumnType.java b/serverdata/src/main/java/zone/themcgamer/data/mysql/data/column/ColumnType.java new file mode 100644 index 0000000..3bc4927 --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/mysql/data/column/ColumnType.java @@ -0,0 +1,18 @@ +package zone.themcgamer.data.mysql.data.column; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author Braydon + * @implNote A list of supported column types and its max length + */ +@AllArgsConstructor @Getter +public enum ColumnType { + VARCHAR(65_535), + INT(11), + LONG(11), + DOUBLE(11); + + private final int maxLength; +} \ No newline at end of file diff --git a/serverdata/src/main/java/zone/themcgamer/data/mysql/data/column/impl/DoubleColumn.java b/serverdata/src/main/java/zone/themcgamer/data/mysql/data/column/impl/DoubleColumn.java new file mode 100644 index 0000000..b720dce --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/mysql/data/column/impl/DoubleColumn.java @@ -0,0 +1,25 @@ +package zone.themcgamer.data.mysql.data.column.impl; + +import lombok.Getter; +import zone.themcgamer.data.mysql.data.Table; +import zone.themcgamer.data.mysql.data.column.Column; +import zone.themcgamer.data.mysql.data.column.ColumnType; + +/** + * @author Braydon + * @implNote This class represents an {@link Double} column in a MySQL {@link Table} + */ +@Getter +public class DoubleColumn extends Column { + public DoubleColumn(String name, Double value) { + super(name, value); + } + + public DoubleColumn(String name, boolean nullable) { + this(name, 0, nullable); + } + + public DoubleColumn(String name, int length, boolean nullable) { + super(name, ColumnType.DOUBLE, length, nullable); + } +} \ No newline at end of file diff --git a/serverdata/src/main/java/zone/themcgamer/data/mysql/data/column/impl/IntegerColumn.java b/serverdata/src/main/java/zone/themcgamer/data/mysql/data/column/impl/IntegerColumn.java new file mode 100644 index 0000000..2a9c256 --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/mysql/data/column/impl/IntegerColumn.java @@ -0,0 +1,29 @@ +package zone.themcgamer.data.mysql.data.column.impl; + +import lombok.Getter; +import zone.themcgamer.data.mysql.data.Table; +import zone.themcgamer.data.mysql.data.column.Column; +import zone.themcgamer.data.mysql.data.column.ColumnType; + +/** + * @author Braydon + * @implNote This class represents an {@link Integer} column in a MySQL {@link Table} + */ +@Getter +public class IntegerColumn extends Column { + private final boolean autoIncrement; + + public IntegerColumn(String name, Integer value) { + super(name, value); + autoIncrement = false; + } + + public IntegerColumn(String name, boolean autoIncrement, boolean nullable) { + this(name, 0, autoIncrement, nullable); + } + + public IntegerColumn(String name, int length, boolean autoIncrement, boolean nullable) { + super(name, ColumnType.INT, length, nullable); + this.autoIncrement = autoIncrement; + } +} \ No newline at end of file diff --git a/serverdata/src/main/java/zone/themcgamer/data/mysql/data/column/impl/LongColumn.java b/serverdata/src/main/java/zone/themcgamer/data/mysql/data/column/impl/LongColumn.java new file mode 100644 index 0000000..1bbfc16 --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/mysql/data/column/impl/LongColumn.java @@ -0,0 +1,25 @@ +package zone.themcgamer.data.mysql.data.column.impl; + +import lombok.Getter; +import zone.themcgamer.data.mysql.data.Table; +import zone.themcgamer.data.mysql.data.column.Column; +import zone.themcgamer.data.mysql.data.column.ColumnType; + +/** + * @author Braydon + * @implNote This class represents an {@link Long} column in a MySQL {@link Table} + */ +@Getter +public class LongColumn extends Column { + public LongColumn(String name, Long value) { + super(name, value); + } + + public LongColumn(String name, boolean nullable) { + this(name, 0, nullable); + } + + public LongColumn(String name, int length, boolean nullable) { + super(name, ColumnType.LONG, length, nullable); + } +} \ No newline at end of file diff --git a/serverdata/src/main/java/zone/themcgamer/data/mysql/data/column/impl/StringColumn.java b/serverdata/src/main/java/zone/themcgamer/data/mysql/data/column/impl/StringColumn.java new file mode 100644 index 0000000..5b3e395 --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/mysql/data/column/impl/StringColumn.java @@ -0,0 +1,19 @@ +package zone.themcgamer.data.mysql.data.column.impl; + +import zone.themcgamer.data.mysql.data.Table; +import zone.themcgamer.data.mysql.data.column.Column; +import zone.themcgamer.data.mysql.data.column.ColumnType; + +/** + * @author Braydon + * @implNote This class represents an {@link String} column in a MySQL {@link Table} + */ +public class StringColumn extends Column { + public StringColumn(String name, String value) { + super(name, value); + } + + public StringColumn(String name, int length, boolean nullable) { + super(name, ColumnType.VARCHAR, length, nullable); + } +} \ No newline at end of file diff --git a/serverdata/src/main/java/zone/themcgamer/data/mysql/repository/MySQLRepository.java b/serverdata/src/main/java/zone/themcgamer/data/mysql/repository/MySQLRepository.java new file mode 100644 index 0000000..167c844 --- /dev/null +++ b/serverdata/src/main/java/zone/themcgamer/data/mysql/repository/MySQLRepository.java @@ -0,0 +1,187 @@ +package zone.themcgamer.data.mysql.repository; + +import com.zaxxer.hikari.HikariDataSource; +import lombok.AllArgsConstructor; +import org.jetbrains.annotations.Nullable; +import zone.themcgamer.data.mysql.data.column.Column; + +import java.sql.*; +import java.util.function.Consumer; + +/** + * @author Braydon + */ +@AllArgsConstructor +public class MySQLRepository { + protected final HikariDataSource dataSource; + + /** + * Insert the given columns using the provided query + * @param query the query to execute + * @param columns the columns to insert + * @return the amount of rows affected + */ + protected int executeInsert(String query, Column[] columns) { + return executeInsert(query, columns, null); + } + + /** + * Insert the given columns using the provided query + * @param query the query to execute + * @param columns the columns to insert + * @param onComplete the oncomplete consumer + * @return the amount of rows affected + */ + protected int executeInsert(String query, Column[] columns, @Nullable Consumer onComplete) { + return executeInsert(query, columns, onComplete, null); + } + + /** + * Insert the given columns using the provided query + * @param query the query to execute + * @param columns the columns to insert + * @param onComplete the oncomplete consumer + * @param onException the exception consumer + * @return the amount of rows affected + */ + protected int executeInsert(String query, Column[] columns, @Nullable Consumer onComplete, + @Nullable Consumer onException) { + try (Connection connection = dataSource.getConnection()) { + return executeInsert(connection, query, columns, onComplete, onException); + } catch (SQLException ex) { + ex.printStackTrace(); + } + return 0; + } + + /** + * Insert the given columns using the provided query + * @param connection the connection to execute the query on + * @param query the query to execute + * @param columns the columns to insert + * @return the amount of rows affected + */ + protected int executeInsert(Connection connection, String query, Column[] columns) { + return executeInsert(connection, query, columns, null); + } + + /** + * Insert the given columns using the provided query + * @param connection the connection to execute the query on + * @param query the query to execute + * @param columns the columns to insert + * @param onComplete the oncomplete consumer + * @return the amount of rows affected + */ + protected int executeInsert(Connection connection, String query, Column[] columns, @Nullable Consumer onComplete) { + return executeInsert(connection, query, columns, onComplete, null); + } + + /** + * Insert the given columns using the provided query + * @param connection the connection to execute the query on + * @param query the query to execute + * @param columns the columns to insert + * @param onComplete the oncomplete consumer + * @param onException the exception consumer + * @return the amount of rows affected + */ + protected int executeInsert(Connection connection, String query, Column[] columns, @Nullable Consumer onComplete, + @Nullable Consumer onException) { + int questionMarks = 0; + for (char character : query.toCharArray()) { + if (character == '?') { + questionMarks++; + } + } + if (questionMarks != columns.length) + throw new IllegalArgumentException("Invalid amount of columns for query \"" + query + "\""); + int affectedRows = 0; + try (PreparedStatement statement = connection.prepareStatement(query, Statement.RETURN_GENERATED_KEYS)) { + int columnIndex = 1; + for (Column column : columns) + statement.setString(columnIndex++, column.getValue() == null ? null : column.getValue().toString()); + affectedRows = statement.executeUpdate(); + if (onComplete != null) + onComplete.accept(statement.getGeneratedKeys()); + } catch (SQLException ex) { + if (onException != null) + onException.accept(ex); + ex.printStackTrace(); + } + return affectedRows; + } + + /** + * Execute the given query + * @param query the query to execute + * @param columns the columns to use in the query + * @param onComplete the oncomplete consumer + */ + protected void executeQuery(String query, Column[] columns, Consumer onComplete) { + executeQuery(query, columns, onComplete, null); + } + + /** + * Execute the given query + * @param query the query to execute + * @param columns the columns to use in the query + * @param onComplete the oncomplete consumer + * @param onException the exception consumer + */ + protected void executeQuery(String query, Column[] columns, Consumer onComplete, + @Nullable Consumer onException) { + try (Connection connection = dataSource.getConnection()) { + executeQuery(connection, query, columns, onComplete, onException); + } catch (SQLException ex) { + ex.printStackTrace(); + } + } + + /** + * Execute the given query + * @param connection the connection to execute the query on + * @param query the query to execute + * @param columns the columns to use in the query + * @param onComplete the oncomplete consumer + */ + protected void executeQuery(Connection connection, String query, Column[] columns, Consumer onComplete) { + executeQuery(connection, query, columns, onComplete, null); + } + + /** + * Execute the given query + * @param connection the connection to execute the query on + * @param query the query to execute + * @param columns the columns to use in the query + * @param onComplete the oncomplete consumer + * @param onException the exception consumer + */ + protected void executeQuery(Connection connection, String query, Column[] columns, Consumer onComplete, + @Nullable Consumer onException) { + int questionMarks = 0; + for (char character : query.toCharArray()) { + if (character == '?') { + questionMarks++; + } + } + if (questionMarks != columns.length) + throw new IllegalArgumentException("Invalid amount of columns for query \"" + query + "\""); + try (PreparedStatement statement = connection.prepareStatement(query)) { + int columnIndex = 1; + for (Column column : columns) + statement.setString(columnIndex++, (column.getValue() == null ? null : column.getValue().toString())); + try (ResultSet resultSet = statement.executeQuery()) { + onComplete.accept(resultSet); + } catch (SQLException ex) { + if (onException != null) + onException.accept(ex); + ex.printStackTrace(); + } + } catch (SQLException ex) { + if (onException != null) + onException.accept(ex); + ex.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..2041e36 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,16 @@ +// PLEASE keep this sorted by importance + +rootProject.name = "McGamerCore" + +include("commons") +include("serverdata") +include("servercontroller") +include("proxy") +include("api") +include("core") +include("buildserver") +include("hub") +include("arcade") +include("skyblock") +include("discordbot") +include("testing") \ No newline at end of file diff --git a/skyblock/build.gradle.kts b/skyblock/build.gradle.kts new file mode 100644 index 0000000..1a69000 --- /dev/null +++ b/skyblock/build.gradle.kts @@ -0,0 +1,27 @@ +repositories { + maven { + url = uri("https://repo.extendedclip.com/content/repositories/placeholderapi/") + } +} + +dependencies { + implementation(project(":core")) + compileOnly("com.destroystokyo:paperspigot:1.12.2") + implementation("com.github.cryptomorin:XSeries:7.8.0") + compileOnly("me.clip:placeholderapi:2.10.9") + compileOnly("com.bgsoftware:superiorskyblock:b59") +} + +tasks { + processResources { + val tokens = mapOf("version" to project.version) + from(sourceSets["main"].resources.srcDirs) { + filter("tokens" to tokens) + } + } + + shadowJar { + archiveFileName.set("${project.rootProject.name}-${project.name}-v${project.version}.jar") + destinationDir = file("$rootDir/output") + } +} \ No newline at end of file diff --git a/skyblock/src/main/java/zone/themcgamer/skyblock/Skyblock.java b/skyblock/src/main/java/zone/themcgamer/skyblock/Skyblock.java new file mode 100644 index 0000000..845bad6 --- /dev/null +++ b/skyblock/src/main/java/zone/themcgamer/skyblock/Skyblock.java @@ -0,0 +1,66 @@ +package zone.themcgamer.skyblock; + +import com.bgsoftware.superiorskyblock.api.SuperiorSkyblockAPI; +import com.bgsoftware.superiorskyblock.api.wrappers.SuperiorPlayer; +import lombok.Getter; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitRunnable; +import zone.themcgamer.core.chat.ChatManager; +import zone.themcgamer.core.chat.component.IChatComponent; +import zone.themcgamer.core.chat.component.impl.BasicNameComponent; +import zone.themcgamer.core.chat.component.impl.BasicRankComponent; +import zone.themcgamer.core.common.scoreboard.ScoreboardHandler; +import zone.themcgamer.core.plugin.MGZPlugin; +import zone.themcgamer.core.plugin.Startup; +import zone.themcgamer.skyblock.chat.SkyblockChatLevelComponent; +import zone.themcgamer.skyblock.commands.StartCommand; +import zone.themcgamer.skyblock.listener.PlayerListener; +import zone.themcgamer.skyblock.scoreboard.SkyblockScoreboard; + +@Getter +public class Skyblock extends MGZPlugin { + public static Skyblock INSTANCE; + + @Override + public void onEnable() { + super.onEnable(); + INSTANCE = this; + } + + @Startup + public void loadSkyblockServer() { + long time = System.currentTimeMillis(); + new PlayerListener(this); + new ScoreboardHandler(this, SkyblockScoreboard.class, 3L); + + new ChatManager(this, badSportSystem, new IChatComponent[] { + new SkyblockChatLevelComponent(), + new BasicRankComponent(), + new BasicNameComponent() + }); + + commandManager.registerCommand(this, new StartCommand()); + getServer().getConsoleSender().sendMessage("Loaded skyblock module in " + (System.currentTimeMillis() - time) + "ms!"); + + new BukkitRunnable() { + @Override + public void run() { + Bukkit.broadcastMessage("Recalculating islands..."); + SuperiorSkyblockAPI.calcAllIslands(); + Bukkit.broadcastMessage("Completed!"); + } + }.runTaskTimer(this,0, 900 * 20); + + new BukkitRunnable() { + @Override + public void run() { + for (Player onlinePlayer : getServer().getOnlinePlayers()) { + SuperiorPlayer superiorPlayer = SuperiorSkyblockAPI.getPlayer(onlinePlayer); + if (superiorPlayer.getIsland() == null) + onlinePlayer.sendTitle("§bGet Started!", "§7Create an island - §e/start"); + } + } + }.runTaskTimer(this,0, 10 * 20); + } +} diff --git a/skyblock/src/main/java/zone/themcgamer/skyblock/chat/SkyblockChatLevelComponent.java b/skyblock/src/main/java/zone/themcgamer/skyblock/chat/SkyblockChatLevelComponent.java new file mode 100644 index 0000000..57863b8 --- /dev/null +++ b/skyblock/src/main/java/zone/themcgamer/skyblock/chat/SkyblockChatLevelComponent.java @@ -0,0 +1,45 @@ +package zone.themcgamer.skyblock.chat; + +import com.bgsoftware.superiorskyblock.api.SuperiorSkyblockAPI; +import com.bgsoftware.superiorskyblock.api.wrappers.SuperiorPlayer; +import me.clip.placeholderapi.PlaceholderAPI; +import net.md_5.bungee.api.chat.*; +import org.bukkit.entity.Player; +import zone.themcgamer.common.DoubleUtils; +import zone.themcgamer.common.MiscUtils; +import zone.themcgamer.core.account.Account; +import zone.themcgamer.core.account.AccountManager; +import zone.themcgamer.core.chat.component.IChatComponent; +import zone.themcgamer.core.common.Style; + +import java.util.Optional; + +public class SkyblockChatLevelComponent implements IChatComponent { + @Override + public BaseComponent getComponent(Player player) { + Optional optionalAccount = AccountManager.fromCache(player.getUniqueId()); + SuperiorPlayer superiorPlayer = SuperiorSkyblockAPI.getPlayer(player); + if (optionalAccount.isEmpty() || (superiorPlayer == null || (superiorPlayer.getIsland() == null))) + return new TextComponent(Style.color("&a[0]")); + double level = Double.parseDouble(String.valueOf(superiorPlayer.getIsland().getIslandLevel())); + ComponentBuilder componentBuilder = new ComponentBuilder(Style.color("&a[" + DoubleUtils.format(level, true) + "]")); + componentBuilder.event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder(MiscUtils.arrayToString( + "", + Style.color("§e┋ &lAccount"), + Style.color("§e┋ §fRank: &7" + optionalAccount.get().getPrimaryRank().getColor() + optionalAccount.get().getPrimaryRank().getDisplayName()), + Style.color("§e┋ &fMoney: &d" + PlaceholderAPI.setPlaceholders(player, "%vault_eco_balance_formatted%")), + Style.color("§e┋ &fMob Gems: &a0"), + Style.color("§e┋ &fMcMMO: &60"), + "", + Style.color("§c┋ &lIsland &7(" + superiorPlayer.getIsland().getName() + "&7)"), + Style.color("§c┋ §fRole: &c" + superiorPlayer.getPlayerRole().getName()), + Style.color("§c┋ §fLevel: &a" + superiorPlayer.getIsland().getIslandLevel()), + Style.color("§c┋ &fSize: &b" + superiorPlayer.getIsland().getIslandSize() + "x" + superiorPlayer.getIsland().getIslandSize()), + Style.color("§c┋ &fTeam: &3" + superiorPlayer.getIsland().getIslandMembers(true).size() + "/" + superiorPlayer.getIsland().getTeamLimit()), + "", + Style.color("&eClick to visit this island!") + )).create())); + componentBuilder.event(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/island visit " + superiorPlayer.getIsland().getName())).create(); + return new TextComponent(componentBuilder.create()); + } +} diff --git a/skyblock/src/main/java/zone/themcgamer/skyblock/commands/StartCommand.java b/skyblock/src/main/java/zone/themcgamer/skyblock/commands/StartCommand.java new file mode 100644 index 0000000..6ebd4e8 --- /dev/null +++ b/skyblock/src/main/java/zone/themcgamer/skyblock/commands/StartCommand.java @@ -0,0 +1,19 @@ +package zone.themcgamer.skyblock.commands; + +import com.bgsoftware.superiorskyblock.api.SuperiorSkyblockAPI; +import com.bgsoftware.superiorskyblock.api.wrappers.SuperiorPlayer; +import zone.themcgamer.core.command.Command; +import zone.themcgamer.core.command.CommandProvider; +import zone.themcgamer.core.common.Style; + +public class StartCommand { + @Command(name = "start", description = "Start your island", playersOnly = true) + public void onCommand(CommandProvider command) { + SuperiorPlayer superiorPlayer = SuperiorSkyblockAPI.getPlayer(command.getPlayer()); + if (superiorPlayer == null) + return; + if (superiorPlayer.getIsland() == null) + SuperiorSkyblockAPI.getSuperiorSkyblock().getMenus().openIslandCreationMenu(superiorPlayer, superiorPlayer.getName()); + else command.getPlayer().sendMessage(Style.main("Skyblock", "You already have an island!")); + } +} diff --git a/skyblock/src/main/java/zone/themcgamer/skyblock/listener/PlayerListener.java b/skyblock/src/main/java/zone/themcgamer/skyblock/listener/PlayerListener.java new file mode 100644 index 0000000..2042bea --- /dev/null +++ b/skyblock/src/main/java/zone/themcgamer/skyblock/listener/PlayerListener.java @@ -0,0 +1,25 @@ +package zone.themcgamer.skyblock.listener; + +import org.bukkit.Bukkit; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import zone.themcgamer.skyblock.Skyblock; + +public class PlayerListener implements Listener { + public PlayerListener(Skyblock skyblock) { + Bukkit.getPluginManager().registerEvents(this, skyblock); + } + + @EventHandler(priority = EventPriority.HIGHEST) + private void onJoin(PlayerJoinEvent event) { + event.setJoinMessage(null); + } + + @EventHandler(priority = EventPriority.HIGHEST) + private void onQuit(PlayerQuitEvent event) { + event.setQuitMessage(null); + } +} diff --git a/skyblock/src/main/java/zone/themcgamer/skyblock/scoreboard/SkyblockScoreboard.java b/skyblock/src/main/java/zone/themcgamer/skyblock/scoreboard/SkyblockScoreboard.java new file mode 100644 index 0000000..3567340 --- /dev/null +++ b/skyblock/src/main/java/zone/themcgamer/skyblock/scoreboard/SkyblockScoreboard.java @@ -0,0 +1,64 @@ +package zone.themcgamer.skyblock.scoreboard; + +import com.bgsoftware.superiorskyblock.api.SuperiorSkyblockAPI; +import com.bgsoftware.superiorskyblock.api.wrappers.SuperiorPlayer; +import me.clip.placeholderapi.PlaceholderAPI; +import org.bukkit.entity.Player; +import zone.themcgamer.common.DoubleUtils; +import zone.themcgamer.core.account.Account; +import zone.themcgamer.core.account.AccountManager; +import zone.themcgamer.core.common.scoreboard.WritableScoreboard; + +import java.time.LocalDateTime; +import java.util.Optional; + +public class SkyblockScoreboard extends WritableScoreboard { + public SkyblockScoreboard(Player player) { + super(player); + } + + @Override + public String getTitle() { + return "§2§lSkyblock §7Pirate"; + } + + @Override + public void writeLines() { + Optional optionalAccount = AccountManager.fromCache(player.getUniqueId()); + if (optionalAccount.isEmpty()) { + writeBlank(); + return; + } + Account account = optionalAccount.get(); + + LocalDateTime dateTime = LocalDateTime.now(); + write("§7" + dateTime.getMonth().getValue() + "/" + dateTime.getDayOfMonth() + "/" + dateTime.getYear()); + + writeBlank(); + write("§e┋ Account"); + write("§e┋ §fRank: &7" + account.getPrimaryRank().getColor() + account.getPrimaryRank().getDisplayName()); + write("§e┋ &fMoney: &d" + PlaceholderAPI.setPlaceholders(player, "%vault_eco_balance_formatted%")); + write("§e┋ &fMob Gems: &a0"); + write("§e┋ &fMcMMO: &60"); + + SuperiorPlayer superiorPlayer = SuperiorSkyblockAPI.getPlayer(player); + if (superiorPlayer == null) + return; + if (superiorPlayer.getIsland() == null) { + writeBlank(); + write("&7You do not have"); + write("&7an island!"); + write("&e/start &7to get started!"); + } else { + writeBlank(); + write("§c┋ Island"); + write("§c┋ §fRole: &c" + superiorPlayer.getPlayerRole().getName()); + write("§c┋ §fLevel: &a" + superiorPlayer.getIsland().getIslandLevel()); + write("§c┋ &fSize: &b" + superiorPlayer.getIsland().getIslandSize() + "x" + superiorPlayer.getIsland().getIslandSize()); + write("§c┋ &fTeam: &3" + superiorPlayer.getIsland().getIslandMembers(true).size() + "/" + superiorPlayer.getIsland().getTeamLimit()); + write("§c┋ &fBank: &e" + DoubleUtils.format(superiorPlayer.getIsland().getIslandBank().getBalance().doubleValue(), true)); + } + writeBlank(); + write("§bthemcgamer.zone"); + } +} \ No newline at end of file diff --git a/skyblock/src/main/resources/plugin.yml b/skyblock/src/main/resources/plugin.yml new file mode 100644 index 0000000..bd4c6a2 --- /dev/null +++ b/skyblock/src/main/resources/plugin.yml @@ -0,0 +1,8 @@ +name: Skyblock +version: 1.0-SNAPSHOT +api-version: 1.13 +main: zone.themcgamer.skyblock.Skyblock +author: MGZ Development Team +softdepend: + - PlaceholderAPI + - SuperiorSkyblock2 \ No newline at end of file diff --git a/testing/META-INF/MANIFEST.MF b/testing/META-INF/MANIFEST.MF new file mode 100644 index 0000000..f99b583 --- /dev/null +++ b/testing/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: zone.themcgamer.controller.ServerController + diff --git a/testing/build.gradle.kts b/testing/build.gradle.kts new file mode 100644 index 0000000..c63da1c --- /dev/null +++ b/testing/build.gradle.kts @@ -0,0 +1,3 @@ +dependencies { + api(project(":serverdata")) +} \ No newline at end of file diff --git a/testing/src/main/java/zone/themcgamer/test/Test.java b/testing/src/main/java/zone/themcgamer/test/Test.java new file mode 100644 index 0000000..c66ee1e --- /dev/null +++ b/testing/src/main/java/zone/themcgamer/test/Test.java @@ -0,0 +1,30 @@ +package zone.themcgamer.test; + +import lombok.SneakyThrows; +import zone.themcgamer.data.jedis.JedisController; +import zone.themcgamer.data.jedis.data.ServerGroup; +import zone.themcgamer.data.jedis.repository.RedisRepository; +import zone.themcgamer.data.jedis.repository.impl.ServerGroupRepository; + +import java.util.List; +import java.util.Optional; + +/** + * @author Braydon + * @implNote This class is strictly for testing purposes + */ +public class Test { + @SneakyThrows + public static void main(String[] args) { + new JedisController().start(); + + Optional repository = RedisRepository.getRepository(ServerGroupRepository.class); + while (repository.isPresent()) { + List cached = repository.get().getCached(); + for (ServerGroup serverGroup : cached) + System.out.println(serverGroup.toString()); + System.out.println("total cached = " + cached.size()); + Thread.sleep(1000L); + } + } +} \ No newline at end of file