commit f23c44bbd2ef4a1e75f72b396fd547332952f9ca Author: thetrouper Date: Sun Jun 1 08:59:14 2025 -0500 Initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b63da45 --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..a0ccf77 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,5 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Environment-dependent path to Maven home directory +/mavenHomeManager.xml diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..d28243b --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..5cd9a10 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..0f3a155 --- /dev/null +++ b/build.gradle @@ -0,0 +1,59 @@ +plugins { + id 'java' + id("xyz.jpenilla.run-paper") version "2.3.1" +} + +group = 'me.trouper' +version = '1.0-SNAPSHOT' + +repositories { + mavenCentral() + maven { + name = "papermc-repo" + url = "https://repo.papermc.io/repository/maven-public/" + } + maven { + name = "sonatype" + url = "https://oss.sonatype.org/content/groups/public/" + } +} + +dependencies { + compileOnly("io.papermc.paper:paper-api:1.21.5-R0.1-SNAPSHOT") +} + +tasks { + runServer { + // Configure the Minecraft version for our task. + // This is the only required configuration besides applying the plugin. + // Your plugin's jar (or shadowJar if present) will be used automatically. + minecraftVersion("1.21.5") + } +} + +def targetJavaVersion = 21 +java { + def javaVersion = JavaVersion.toVersion(targetJavaVersion) + sourceCompatibility = javaVersion + targetCompatibility = javaVersion + if (JavaVersion.current() < javaVersion) { + toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion) + } +} + +tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' + + if (targetJavaVersion >= 10 || JavaVersion.current().isJava10Compatible()) { + options.release.set(targetJavaVersion) + } +} + +processResources { + def props = [version: version] + inputs.properties props + filteringCharset 'UTF-8' + filesMatching('plugin.yml') { + expand props + } +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..2c04b59 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu May 29 23:20:25 CDT 2025 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/run/plugins/Guns-0.0.9.jar b/run/plugins/Guns-0.0.9.jar new file mode 100644 index 0000000..e762114 Binary files /dev/null and b/run/plugins/Guns-0.0.9.jar differ diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..89791b4 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'Alias' \ No newline at end of file diff --git a/src/main/java/me/trouper/alias/Alias.java b/src/main/java/me/trouper/alias/Alias.java new file mode 100644 index 0000000..482a96e --- /dev/null +++ b/src/main/java/me/trouper/alias/Alias.java @@ -0,0 +1,46 @@ +package me.trouper.alias; + +import me.trouper.alias.server.Manager; +import me.trouper.alias.utils.visual.BlockDisplayRaytracer; +import org.bukkit.NamespacedKey; +import org.bukkit.plugin.java.JavaPlugin; + +public final class Alias extends JavaPlugin { + + private static Alias instance; + private Manager manager; + + @Override + public void onLoad() { + getLogger().info("Instantiating Plugin"); + instance = this; + } + + @Override + public void onEnable() { + getLogger().info("Instantiating Manager"); + manager = new Manager(); + + getLogger().info("Initializing Manager"); + manager.init(); + + getLogger().info("Successfully enabled TrimAlias."); + } + + @Override + public void onDisable() { + getLogger().info("Saved all IO files."); + manager.io.saveAll(); + getLogger().info("Saved all IO files."); + } + + public static me.trouper.alias.Alias getInstance() { + return instance; + } + public NamespacedKey getNameSpace() { + return new NamespacedKey(getInstance(),"_alias"); + } + public Manager getManager() { + return manager; + } +} diff --git a/src/main/java/me/trouper/alias/data/IO.java b/src/main/java/me/trouper/alias/data/IO.java new file mode 100755 index 0000000..726ed66 --- /dev/null +++ b/src/main/java/me/trouper/alias/data/IO.java @@ -0,0 +1,37 @@ +package me.trouper.alias.data; + +import me.trouper.alias.Alias; +import me.trouper.alias.data.io.Config; +import me.trouper.alias.data.io.Storage; +import me.trouper.alias.utils.misc.JsonSerializable; + +import java.io.File; + +public class IO { + public final File DATA_FOLDER; + public final File CONFIG_FILE; + public final File STORAGE_FILE; + + public Config config; + public Storage storage; + + public IO() { + DATA_FOLDER = new File("plugins/Alias"); + CONFIG_FILE = new File(DATA_FOLDER,"/config.json"); + STORAGE_FILE = new File(DATA_FOLDER,"/storage.json"); + config = JsonSerializable.load(CONFIG_FILE,Config.class,new Config()); + storage = JsonSerializable.load(STORAGE_FILE,Storage.class,new Storage()); + } + + public void loadAll() { + me.trouper.alias.Alias.getInstance().getLogger().info("Loading all IO Files"); + config = JsonSerializable.load(CONFIG_FILE,Config.class,new Config()); + storage = JsonSerializable.load(STORAGE_FILE,Storage.class,new Storage()); + } + + public void saveAll() { + me.trouper.alias.Alias.getInstance().getLogger().info("Saving all IO Files"); + config.save(); + storage.save(); + } +} diff --git a/src/main/java/me/trouper/alias/data/io/Config.java b/src/main/java/me/trouper/alias/data/io/Config.java new file mode 100755 index 0000000..f84379c --- /dev/null +++ b/src/main/java/me/trouper/alias/data/io/Config.java @@ -0,0 +1,34 @@ +package me.trouper.alias.data.io; + +import me.trouper.alias.utils.Verbose; +import me.trouper.alias.utils.misc.JsonSerializable; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +public class Config implements JsonSerializable { + + public boolean debugMode = false; + public List debuggerExclusions = new ArrayList<>(); + + @Override + public File getFile() { + return me.trouper.alias.Alias.getInstance().getManager().io.CONFIG_FILE; + } + + @Override + public void save() { + Verbose.send(1,"Saving Config..."); + JsonSerializable.super.save(); + } + + public Messages messages = new Messages(); + + public class Messages { + public String mainColor = "&#ffaaff"; + public String prefix = "&9Alias> &7"; + public String pluginName = "Alias"; + public boolean fancyAlerts = true; + } +} diff --git a/src/main/java/me/trouper/alias/data/io/Storage.java b/src/main/java/me/trouper/alias/data/io/Storage.java new file mode 100755 index 0000000..0995bb7 --- /dev/null +++ b/src/main/java/me/trouper/alias/data/io/Storage.java @@ -0,0 +1,22 @@ +package me.trouper.alias.data.io; + +import me.trouper.alias.utils.Verbose; +import me.trouper.alias.utils.misc.JsonSerializable; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +public class Storage implements JsonSerializable { + + @Override + public File getFile() { + return me.trouper.alias.Alias.getInstance().getManager().io.STORAGE_FILE; + } + + @Override + public void save() { + Verbose.send(1,"Saving Storage..."); + JsonSerializable.super.save(); + } +} diff --git a/src/main/java/me/trouper/alias/server/Main.java b/src/main/java/me/trouper/alias/server/Main.java new file mode 100755 index 0000000..372b12d --- /dev/null +++ b/src/main/java/me/trouper/alias/server/Main.java @@ -0,0 +1,71 @@ +package me.trouper.alias.server; + +import io.papermc.paper.registry.RegistryAccess; +import me.trouper.alias.data.IO; +import me.trouper.alias.data.io.Config; +import me.trouper.alias.data.io.Storage; +import me.trouper.alias.utils.Text; +import org.bukkit.command.CommandSender; + +import java.util.Random; +import java.util.function.BooleanSupplier; + +public interface Main { + Main main = new Main() {}; + + Random random = new Random(); + + default RegistryAccess getRegistryAccess() { + return RegistryAccess.registryAccess(); + } + + default me.trouper.alias.Alias getPlugin() { + return me.trouper.alias.Alias.getInstance(); + } + + default Manager man() { + return getPlugin().getManager(); + } + + default IO io() { + return man().io; + }; + + default Config config() { + return io().config; + } + + default Storage storage() { + return io().storage; + } + + default void info(CommandSender player, String message, Object... args) { + Text.sendMessage(Text.Pallet.INFO, player, message, args); + } + + default void error(CommandSender player, String message, Object... args) { + Text.sendMessage(Text.Pallet.ERROR, player, message, args); + } + + default void warning(CommandSender player, String message, Object... args) { + Text.sendMessage(Text.Pallet.WARNING, player, message, args); + } + + default void success(CommandSender player, String message, Object... args) { + Text.sendMessage(Text.Pallet.SUCCESS, player, message, args); + } + + default void message(CommandSender player, String message, Object... args) { + Text.sendMessage(Text.Pallet.NEUTRAL, player, message, args); + } + + default void checkPre(boolean check, String msg, Object... args) { + if (!check) { + throw new IllegalArgumentException(msg.formatted(args)); + } + } + + default void checkPre(BooleanSupplier check, String msg, Object... args) { + checkPre(check.getAsBoolean(), msg, args); + } +} diff --git a/src/main/java/me/trouper/alias/server/Manager.java b/src/main/java/me/trouper/alias/server/Manager.java new file mode 100755 index 0000000..a9d2f73 --- /dev/null +++ b/src/main/java/me/trouper/alias/server/Manager.java @@ -0,0 +1,43 @@ +package me.trouper.alias.server; + +import me.trouper.alias.data.IO; +import me.trouper.alias.server.commands.ExampleCommand; +import me.trouper.alias.server.events.ExampleEvent; +import me.trouper.alias.server.systems.gui.RegistryListeners; +import me.trouper.alias.utils.visual.BlockDisplayRaytracer; +import org.bukkit.Bukkit; + +public class Manager { + public IO io; + // Define backends here + + public Manager() { + io = new IO(); + // Instantiate backends here + } + + + public void init() { + io.loadAll(); + + registerEvents(); + registerCommands(); + + cleanup(); + } + + + private void registerCommands() { + new ExampleCommand().register(); + } + + private void registerEvents() { + new RegistryListeners().registerEvents(); // Must be called for any GUI to work! + new ExampleEvent().registerEvents(); + } + + private void cleanup() { + BlockDisplayRaytracer.cleanup(); + } + +} \ No newline at end of file diff --git a/src/main/java/me/trouper/alias/server/commands/ExampleCommand.java b/src/main/java/me/trouper/alias/server/commands/ExampleCommand.java new file mode 100644 index 0000000..06694ae --- /dev/null +++ b/src/main/java/me/trouper/alias/server/commands/ExampleCommand.java @@ -0,0 +1,22 @@ +package me.trouper.alias.server.commands; + +import me.trouper.alias.utils.command.Args; +import me.trouper.alias.utils.command.CommandRegistry; +import me.trouper.alias.utils.command.QuickCommand; +import me.trouper.alias.utils.command.completions.CompletionBuilder; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; + +@CommandRegistry(value = "example") +public class ExampleCommand implements QuickCommand { + + @Override + public void dispatchCommand(CommandSender sender, Command command, String label, Args args) { + success(sender,"Hello, World!"); + } + + @Override + public void dispatchCompletions(CommandSender sender, Command command, String label, CompletionBuilder b) { + + } +} diff --git a/src/main/java/me/trouper/alias/server/events/ExampleEvent.java b/src/main/java/me/trouper/alias/server/events/ExampleEvent.java new file mode 100644 index 0000000..6793326 --- /dev/null +++ b/src/main/java/me/trouper/alias/server/events/ExampleEvent.java @@ -0,0 +1,11 @@ +package me.trouper.alias.server.events; + +import org.bukkit.event.EventHandler; +import org.bukkit.event.player.PlayerJoinEvent; + +public class ExampleEvent implements QuickListener { + @EventHandler + public void onJoin(PlayerJoinEvent e) { + info(e.getPlayer(),"This server is running Alias v{0}.",main.getPlugin().getDescription().getVersion()); + } +} diff --git a/src/main/java/me/trouper/alias/server/events/QuickListener.java b/src/main/java/me/trouper/alias/server/events/QuickListener.java new file mode 100755 index 0000000..1c41f3a --- /dev/null +++ b/src/main/java/me/trouper/alias/server/events/QuickListener.java @@ -0,0 +1,12 @@ +package me.trouper.alias.server.events; + +import me.trouper.alias.server.Main; +import org.bukkit.Bukkit; +import org.bukkit.event.Listener; + +public interface QuickListener extends Listener, Main { + default QuickListener registerEvents() { + Bukkit.getPluginManager().registerEvents(this, this.getPlugin()); + return this; + } +} diff --git a/src/main/java/me/trouper/alias/server/systems/builders/ItemBuilder.java b/src/main/java/me/trouper/alias/server/systems/builders/ItemBuilder.java new file mode 100644 index 0000000..334cfe9 --- /dev/null +++ b/src/main/java/me/trouper/alias/server/systems/builders/ItemBuilder.java @@ -0,0 +1,103 @@ +package me.trouper.alias.server.systems.builders; + +import org.bukkit.Material; +import org.bukkit.attribute.Attribute; +import org.bukkit.attribute.AttributeModifier; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +public class ItemBuilder { + + private ItemStack stack; + private ItemMeta meta; + + public ItemBuilder() { + this(new ItemStack(Material.STONE)); + } + + public ItemBuilder(ItemStack stack) { + this.stack = stack; + this.meta = this.stack.getItemMeta(); + } + + public ItemBuilder material(Material m) { + stack.setType(m); + return this; + } + + public ItemBuilder name(String s) { + meta.setDisplayName(s); + return this; + } + + public ItemBuilder lore(String s) { + List lore = meta.hasLore() ? meta.getLore() : new ArrayList<>(); + lore.add(s); + meta.setLore(lore); + return this; + } + + public ItemBuilder lore(Iterable s) { + s.forEach(this::lore); + return this; + } + + public ItemBuilder count(int c) { + stack.setAmount(c); + return this; + } + + public ItemBuilder enchant(Enchantment e, int level) { + meta.addEnchant(e,level,true); + return this; + } + + public ItemBuilder flag(ItemFlag... f) { + meta.addItemFlags(f); + return this; + } + + public ItemBuilder attribute(Attribute a, AttributeModifier am) { + meta.addAttributeModifier(a,am); + return this; + } + + public ItemBuilder unbreakable(boolean b) { + meta.setUnbreakable(b); + return this; + } + + public ItemBuilder customModelData(int d) { + meta.setCustomModelData(d); + return this; + } + + public ItemBuilder runTaskItem(Function task) { + this.stack = task.apply(build()); + return this; + } + + public ItemBuilder runTaskMeta(Function task) { + this.meta = task.apply(meta); + return this; + } + + public ItemStack build() { + stack.setItemMeta(meta); + return stack; + } + + public static ItemBuilder create() { + return new ItemBuilder(); + } + + public static ItemBuilder create(ItemStack item) { + return new ItemBuilder(item); + } +} \ No newline at end of file diff --git a/src/main/java/me/trouper/alias/server/systems/gui/CustomGui.java b/src/main/java/me/trouper/alias/server/systems/gui/CustomGui.java new file mode 100644 index 0000000..5688d8b --- /dev/null +++ b/src/main/java/me/trouper/alias/server/systems/gui/CustomGui.java @@ -0,0 +1,195 @@ +package me.trouper.alias.server.systems.gui; + +import org.bukkit.Bukkit; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryCloseEvent; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; + +public class CustomGui { + + private static final Map registry = new HashMap<>(); + private final Map slotActions; + private final Map slotDisplays; + private final InvAction mainAction; + private final CreateAction createAction; + private final CloseAction closeAction; + private final String title; + private final int size; + + public CustomGui(String title, int size, InvAction mainAction, Map slotActions, Map slotDisplays, CreateAction createAction, CloseAction closeAction) { + this.slotActions = slotActions; + this.slotDisplays = slotDisplays; + this.mainAction = mainAction; + this.createAction = createAction; + this.closeAction = closeAction; + this.title = title; + this.size = size; + } + + public static CustomGui register(CustomGui gui) { + if (gui != null) { + registry.put(gui.getTitle(), gui); + } + return gui; + } + + public static Map registries() { + return new HashMap<>(registry); + } + + public static void handleRegistriesClick(InventoryClickEvent event) { + String title = event.getView().getTitle(); + if (registry.containsKey(title)) { + registry.get(title).onInventoryClick(event); + } + } + + public static void handleRegistriesClose(InventoryCloseEvent event) { + String title = event.getView().getTitle(); + if (registry.containsKey(title)) { + registry.get(title).onInventoryClose(event); + } + } + + public Inventory getInventory() { + int size = this.size; + + if (size % 9 != 0) { + int max = slotActions.keySet().stream().sorted(Comparator.comparing(i -> (int)i).reversed()).toList().get(0); + int add = max % 9 == 0 ? 0 : 1; + size = (int)(Math.floor(max / 9.0) + add) * 9; + } + + Inventory inv = Bukkit.createInventory(null, size, title); + createAction.onCreate(inv); + slotDisplays.forEach(inv::setItem); + + return inv; + } + + public void onInventoryClick(InventoryClickEvent event) { + if (!event.isCancelled() && event.getClickedInventory() != null && event.getClickedInventory().getType() != InventoryType.PLAYER) { + mainAction.onClick(event); + if (slotActions.containsKey(event.getSlot())) { + slotActions.get(event.getSlot()).onClick(event); + } + } + } + + public void onInventoryClose(InventoryCloseEvent event) { + closeAction.onClose(event); + } + + public String getTitle() { + return title; + } + + public Map getSlotActions() { + return slotActions; + } + + public Map getSlotDisplays() { + return slotDisplays; + } + + public CreateAction getCreateAction() { + return createAction; + } + + public CloseAction getCloseAction() { + return closeAction; + } + + public int getInvSize() { + return size; + } + + + + public static GuiBuilder create() { + return new GuiBuilder(); + } + + public static class GuiBuilder { + private InvAction mainAction; + private CreateAction createAction; + private CloseAction closeAction; + private final Map slotActions; + private final Map slotDisplay; + private String title; + private int size; + + public GuiBuilder() { + this.title = "Untitled Inventory"; + this.size = -1; + this.mainAction = event -> {}; + this.createAction = inv -> {}; + this.closeAction = event -> {}; + this.slotActions = new HashMap<>(); + this.slotDisplay = new HashMap<>(); + } + + public GuiBuilder title(String text) { + title = text; + return this; + } + + public GuiBuilder size(int size) { + this.size = size; + return this; + } + + public GuiBuilder onDefine(CreateAction action) { + createAction = action; + return this; + } + + public GuiBuilder onClose(CloseAction action) { + closeAction = action; + return this; + } + + public GuiBuilder defineMain(InvAction mainAction) { + this.mainAction = mainAction; + return this; + } + + public GuiBuilder define(int slot, ItemStack display, InvAction action) { + if (slot < 0 || slot >= 54 || display == null || action == null) return this; + slotActions.put(slot, action); + slotDisplay.put(slot, display); + return this; + } + + public GuiBuilder define(int slot, ItemStack display) { + return define(slot, display, event -> {}); + } + + public CustomGui build() { + CustomGui gui = new CustomGui(title, size, mainAction, slotActions, slotDisplay, createAction, closeAction); + CustomGui.register(gui); + return gui; + } + } + + @FunctionalInterface + public interface InvAction { + void onClick(InventoryClickEvent event); + } + + @FunctionalInterface + public interface CloseAction { + void onClose(InventoryCloseEvent event); + } + + @FunctionalInterface + public interface CreateAction { + void onCreate(Inventory inv); + } +} diff --git a/src/main/java/me/trouper/alias/server/systems/gui/RegistryListeners.java b/src/main/java/me/trouper/alias/server/systems/gui/RegistryListeners.java new file mode 100644 index 0000000..9acaeb6 --- /dev/null +++ b/src/main/java/me/trouper/alias/server/systems/gui/RegistryListeners.java @@ -0,0 +1,38 @@ +package me.trouper.alias.server.systems.gui; + +import me.trouper.alias.server.events.QuickListener; +import me.trouper.alias.server.systems.items.CustomItem; +import me.trouper.alias.server.systems.items.ItemManager; +import org.bukkit.event.EventHandler; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryCloseEvent; +import org.bukkit.event.player.PlayerInteractEvent; + +public class RegistryListeners implements QuickListener { + + @EventHandler + private void onClick(InventoryClickEvent e) { + try { + CustomGui.handleRegistriesClick(e); + } + catch (Exception ignore) {} + } + + @EventHandler + private void onClose(InventoryCloseEvent e) { + try { + CustomGui.handleRegistriesClose(e); + } + catch (Exception ignore) {} + } + + @EventHandler + private void onInteract(PlayerInteractEvent e) { + try { + CustomItem context = ItemManager.getItemContext(e.getItem(), CustomItem.class); + if (context != null) + context.onInteract(e); + } + catch (Exception ignore) {} + } +} diff --git a/src/main/java/me/trouper/alias/server/systems/items/CustomItem.java b/src/main/java/me/trouper/alias/server/systems/items/CustomItem.java new file mode 100644 index 0000000..2fe9edd --- /dev/null +++ b/src/main/java/me/trouper/alias/server/systems/items/CustomItem.java @@ -0,0 +1,71 @@ +package me.trouper.alias.server.systems.items; + +import com.google.gson.Gson; +import me.trouper.alias.server.Main; +import me.trouper.alias.server.systems.builders.ItemBuilder; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.Arrays; + +public abstract class CustomItem implements Main { + + public abstract void createItem(ItemBuilder item); + + public abstract void updateMeta(ItemMeta meta); + + public abstract void onInteract(PlayerInteractEvent e); + + public String serialize() { + try { + Gson gson = new Gson(); + return gson.toJson(this); + } + catch (Exception ex) { + return "{}"; + } + } + + public String getRegistryKey() { + ItemRegistry r = this.getClass().getAnnotation(ItemRegistry.class); + if (r == null) + throw new IllegalArgumentException("Custom items need to have @ItemRegistry annotation!"); + return r.value(); + } + + public void register(Object... initArgs) { + try { + Class[] signature = Arrays.stream(initArgs).map(Object::getClass).toArray(Class[]::new); + CustomItem item = this.getClass().getDeclaredConstructor(signature).newInstance(initArgs); + ItemManager.registerItem(getRegistryKey(), () -> item); + } + catch (Exception ex) { + throw new RuntimeException("Failed to register custom item: " + ex.getMessage()); + } + } + + public static String getDisplay(ItemStack item) { + if (item == null) { + return ""; + } + else if (item.hasItemMeta() && item.getItemMeta() != null && item.getItemMeta().hasDisplayName()) { + return item.getItemMeta().getDisplayName(); + } + else { + return item.getType().name().toLowerCase(); + } + } + + public static boolean matchDisplay(ItemStack a, ItemStack b) { + return getDisplay(a).equals(getDisplay(b)); + } + + public static boolean matchDisplay(String a, ItemStack b) { + return a.equals(getDisplay(b)); + } + + public static boolean matchDisplay(ItemStack a, String b) { + return getDisplay(a).equals(b); + } +} \ No newline at end of file diff --git a/src/main/java/me/trouper/alias/server/systems/items/ItemManager.java b/src/main/java/me/trouper/alias/server/systems/items/ItemManager.java new file mode 100644 index 0000000..4890a92 --- /dev/null +++ b/src/main/java/me/trouper/alias/server/systems/items/ItemManager.java @@ -0,0 +1,160 @@ +package me.trouper.alias.server.systems.items; + +import com.google.gson.Gson; +import me.trouper.alias.server.Main; +import me.trouper.alias.server.systems.builders.ItemBuilder; +import org.bukkit.NamespacedKey; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; + +public class ItemManager implements Main { + + private static final Gson gson = new Gson(); + private static final Map> itemRegistry = new HashMap<>(); + + public static NamespacedKey registerItem(String namespace, Supplier context) { + NamespacedKey key = new NamespacedKey(main.getPlugin(), "items/" + namespace); + itemRegistry.put(key, context); + return key; + } + + public static ItemStack createItemContext(String namespace) { + NamespacedKey key = new NamespacedKey(main.getPlugin(), "items/" + namespace); + Supplier registry = itemRegistry.get(key); + if (registry == null) + throw new IllegalArgumentException("context not found in item registry"); + + CustomItem context = registry.get(); + ItemBuilder builder = ItemBuilder.create(); + context.createItem(builder); + + ItemStack result = builder.build(); + if (result == null || !result.hasItemMeta()) + throw new IllegalArgumentException("the result item cannot be used as a custom item"); + + ItemMeta meta = result.getItemMeta(); + PersistentDataContainer data = meta.getPersistentDataContainer(); + + data.set(key, PersistentDataType.STRING, gson.toJson(context)); + context.updateMeta(meta); + result.setItemMeta(meta); + + return result; + } + + public static void setItemContext(ItemStack item, CustomItem context) { + if (item == null || !item.hasItemMeta()) + return; + + ItemMeta meta = item.getItemMeta(); + PersistentDataContainer data = meta.getPersistentDataContainer(); + + for (NamespacedKey key : data.getKeys()) { + Supplier contextSupplier = itemRegistry.get(key); + if (contextSupplier == null) + continue; + + CustomItem customItem = contextSupplier.get(); + if (customItem != null && customItem.getClass() == context.getClass()) { + data.set(key, PersistentDataType.STRING, gson.toJson(context)); + context.updateMeta(meta); + item.setItemMeta(meta); + return; + } + } + } + + @SuppressWarnings({ "unchecked", "unused" }) + public static C getItemContext(ItemStack item, Class type) { + if (item == null || !item.hasItemMeta()) + return null; + + ItemMeta meta = item.getItemMeta(); + PersistentDataContainer data = meta.getPersistentDataContainer(); + + for (NamespacedKey key : data.getKeys()) { + Supplier contextSupplier = itemRegistry.get(key); + if (contextSupplier == null) + continue; + + CustomItem customItem = contextSupplier.get(); + if (customItem != null) { + CustomItem parsed = gson.fromJson(data.get(key, PersistentDataType.STRING), customItem.getClass()); + if (parsed != null) + return (C)parsed; + } + } + + return null; + } + + public static C getOrDefItemContext(ItemStack item, Class type, C fallback) { + C context = getItemContext(item, type); + if (context != null) + return context; + + NamespacedKey key = getKeyOf(fallback); + if (key == null) + return fallback; + + ItemMeta meta = item.getItemMeta(); + PersistentDataContainer data = meta.getPersistentDataContainer(); + + data.set(key, PersistentDataType.STRING, gson.toJson(fallback)); + fallback.updateMeta(meta); + item.setItemMeta(meta); + return fallback; + } + + public static NamespacedKey getKeyOf(CustomItem context) { + if (context == null) + return null; + + for (Map.Entry> entry : itemRegistry.entrySet()) + if (entry.getValue().get().getClass() == context.getClass()) + return entry.getKey(); + return null; + } + + public static CustomItem getItemContext(ItemStack item) { + return getItemContext(item, CustomItem.class); + } + + public static boolean isHolding(Player player, NamespacedKey item) { + ItemStack stack = player.getInventory().getItemInMainHand(); + return isItemMatching(item, stack); + } + + public static boolean isOffHolding(Player player, NamespacedKey item) { + ItemStack stack = player.getInventory().getItemInOffHand(); + return isItemMatching(item, stack); + } + + public static boolean isHoldingAny(Player player, NamespacedKey item) { + return isHolding(player, item) || isOffHolding(player, item); + } + + public static boolean isItemMatching(NamespacedKey key, ItemStack item) { + if (item == null || !item.hasItemMeta()) + return false; + + ItemMeta meta = item.getItemMeta(); + PersistentDataContainer data = meta.getPersistentDataContainer(); + return data.has(key); + } + + public static String[] collectNames() { + String[] str = new String[itemRegistry.size()]; + int i = 0; + for (NamespacedKey key : itemRegistry.keySet()) + str[i++] = key.getKey().replaceFirst("items/", ""); + return str; + } +} \ No newline at end of file diff --git a/src/main/java/me/trouper/alias/server/systems/items/ItemRegistry.java b/src/main/java/me/trouper/alias/server/systems/items/ItemRegistry.java new file mode 100644 index 0000000..95f0380 --- /dev/null +++ b/src/main/java/me/trouper/alias/server/systems/items/ItemRegistry.java @@ -0,0 +1,13 @@ +package me.trouper.alias.server.systems.items; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface ItemRegistry { + + String value(); +} diff --git a/src/main/java/me/trouper/alias/utils/ItemUtils.java b/src/main/java/me/trouper/alias/utils/ItemUtils.java new file mode 100755 index 0000000..a50730e --- /dev/null +++ b/src/main/java/me/trouper/alias/utils/ItemUtils.java @@ -0,0 +1,129 @@ +package me.trouper.alias.utils; + +import com.google.common.collect.Multimap; +import org.bukkit.Material; +import org.bukkit.attribute.Attribute; +import org.bukkit.attribute.AttributeModifier; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.List; +import java.util.Map; + +public class ItemUtils { + + public static boolean isArmor(ItemStack i) { + if (i == null || i.isEmpty()) return false; + + return isHelmet(i) || isChestplate(i) || isLeggings(i) || isBoots(i); + } + + public static boolean isHelmet(ItemStack i) { + if (i == null || i.isEmpty()) return false; + + Material m = i.getType(); + String n = m.name(); + return n.contains("HELMET"); + } + + public static boolean isChestplate(ItemStack i) { + if (i == null || i.isEmpty()) return false; + + Material m = i.getType(); + String n = m.name(); + return n.contains("CHESTPLATE"); + } + + public static boolean isLeggings(ItemStack i) { + if (i == null || i.isEmpty()) return false; + + Material m = i.getType(); + String n = m.name(); + return n.contains("LEGGINGS"); + } + + public static boolean isBoots(ItemStack i) { + if (i == null || i.isEmpty()) return false; + + Material m = i.getType(); + String n = m.name(); + return n.contains("BOOTS"); + } + + @SuppressWarnings("deprecation") + public static boolean isSimilar(ItemStack item1, ItemStack item2) { + if (item1 == null || item2 == null) { + Verbose.send("One of the items is null: item1: %s, item2: %s", item1, item2); + return false; + } + + Material type1 = item1.getType(); + Material type2 = item2.getType(); + boolean typeEqual = type1 == type2; + Verbose.send("Checking Material: item1 type: %s, item2 type: %s, equal: %s", type1, type2, typeEqual); + if (!typeEqual) return false; + + boolean hasMeta1 = item1.hasItemMeta(); + boolean hasMeta2 = item2.hasItemMeta(); + boolean metaExistEqual = (hasMeta1 == hasMeta2); + Verbose.send("Checking ItemMeta existence: item1 has meta: %s, item2 has meta: %s, equal: %s", hasMeta1, hasMeta2, metaExistEqual); + if (!metaExistEqual) return false; + + if (!hasMeta1 && !hasMeta2) { + return true; + } + + ItemMeta meta1 = item1.getItemMeta(); + ItemMeta meta2 = item2.getItemMeta(); + + String name1 = meta1.hasDisplayName() ? meta1.getDisplayName() : null; + String name2 = meta2.hasDisplayName() ? meta2.getDisplayName() : null; + if (name1 == null ^ name2 == null) { + Verbose.send("Custom Name mismatch: item1 name: %s, item2 name: %s", name1, name2); + return false; + } + boolean nameEqual = (name1 == null || name1.equals(name2)); + Verbose.send("Checking Custom Name: item1: %s, item2: %s, equal: %s", name1, name2, nameEqual); + if (!nameEqual) return false; + + List lore1 = meta1.hasLore() ? meta1.getLore() : null; + List lore2 = meta2.hasLore() ? meta2.getLore() : null; + if (lore1 == null ^ lore2 == null) { + Verbose.send("Lore mismatch: item1 lore: %s, item2 lore: %s", lore1, lore2); + return false; + } + boolean loreEqual = (lore1 == null || lore1.equals(lore2)); + Verbose.send("Checking Lore: item1: %s, item2: %s, equal: %s", lore1, lore2, loreEqual); + if (!loreEqual) return false; + + int cmd1 = meta1.hasCustomModelData() ? meta1.getCustomModelData() : -1; + int cmd2 = meta2.hasCustomModelData() ? meta2.getCustomModelData() : -1; + boolean cmdEqual = (cmd1 == cmd2); + Verbose.send("Checking Custom Model Data: item1: %d, item2: %d, equal: %s", cmd1, cmd2, cmdEqual); + if (!cmdEqual) return false; + + Map enchants1 = meta1.getEnchants(); + Map enchants2 = meta2.getEnchants(); + if (enchants1 == null ^ enchants2 == null) { + Verbose.send("Enchantments mismatch: item1 enchants: %s, item2 enchants: %s", enchants1, enchants2); + return false; + } + boolean enchantsEqual = (enchants1 == null || enchants1.equals(enchants2)); + Verbose.send("Checking Enchantments: item1: %s, item2: %s, equal: %s", enchants1, enchants2, enchantsEqual); + if (!enchantsEqual) return false; + + Multimap modifiers1 = meta1.getAttributeModifiers(); + Multimap modifiers2 = meta2.getAttributeModifiers(); + if (modifiers1 == null ^ modifiers2 == null) { + Verbose.send("Attribute Modifiers mismatch: item1 modifiers: %s, item2 modifiers: %s", modifiers1, modifiers2); + return false; + } + boolean modifiersEqual = (modifiers1 == null || modifiers1.equals(modifiers2)); + Verbose.send("Checking Attribute Modifiers: item1: %s, item2: %s, equal: %s", modifiers1, modifiers2, modifiersEqual); + if (!modifiersEqual) return false; + + return true; + } + +} diff --git a/src/main/java/me/trouper/alias/utils/PlayerUtils.java b/src/main/java/me/trouper/alias/utils/PlayerUtils.java new file mode 100755 index 0000000..f22205e --- /dev/null +++ b/src/main/java/me/trouper/alias/utils/PlayerUtils.java @@ -0,0 +1,79 @@ +package me.trouper.alias.utils; + +import me.trouper.alias.server.Main; +import org.bukkit.Location; +import org.bukkit.damage.DamageSource; +import org.bukkit.entity.Entity; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.bukkit.util.Vector; + +public class PlayerUtils implements Main { + + public static final int DEFAULT_NO_DAMAGE_TICKS = 9; + public static final int DEFAULT_MAX_NO_DAMAGE_TICKS = 10; + public static final int NO_DAMAGE_TICKS = 1; + public static final int MAX_NO_DAMAGE_TICKS = 2; + + private static float wrapYaw(float yaw) { + yaw = yaw % 360; + if (yaw < 0) yaw += 360; + return yaw; + } + + private static Vector getDirection(float yaw, float pitch) { + double radYaw = Math.toRadians(yaw); + double radPitch = Math.toRadians(pitch); + double x = -Math.cos(radPitch) * Math.sin(radYaw); + double y = -Math.sin(radPitch); + double z = Math.cos(radPitch) * Math.cos(radYaw); + return new Vector(x, y, z); + } + + public static Player playerClosestAngle(Player player, double range) { + Vector playerDirection = player.getEyeLocation().getDirection().normalize(); + Location eyeLoc = player.getEyeLocation(); + + return player.getNearbyEntities(range, range, range).stream() + .filter(entity -> entity instanceof Player && !entity.equals(player)) + .map(entity -> (Player) entity) + .min((p1, p2) -> { + Vector dirToP1 = p1.getEyeLocation().toVector().subtract(eyeLoc.toVector()).normalize(); + Vector dirToP2 = p2.getEyeLocation().toVector().subtract(eyeLoc.toVector()).normalize(); + + double angle1 = playerDirection.angle(dirToP1); + double angle2 = playerDirection.angle(dirToP2); + + return Double.compare(angle1, angle2); + }) + .orElse(null); + } + + public static void dealTrueDamage(LivingEntity target, DamageSource source, double amount) { + if (source.getDirectEntity() instanceof Player a && target instanceof Player t) return; + + target.damage(1, source); + + double newHealth = target.getHealth() - amount; + if (newHealth <= 0) { + target.setHealth(0); + } else { + target.setHealth(newHealth); + } + + Entity attacker = source.getDirectEntity(); + if (attacker instanceof LivingEntity) { + double dx = target.getX() - attacker.getX(); + double dz = target.getZ() - attacker.getZ(); + double magnitude = Math.sqrt(dx * dx + dz * dz); + + if (magnitude > 0) { + double strength = 0.4; + dx /= magnitude; + dz /= magnitude; + target.setVelocity(target.getVelocity().add(new Vector(dx * strength, 0.1, dz * strength))); + } + } + } + +} diff --git a/src/main/java/me/trouper/alias/utils/SoundPlayer.java b/src/main/java/me/trouper/alias/utils/SoundPlayer.java new file mode 100755 index 0000000..6e7919d --- /dev/null +++ b/src/main/java/me/trouper/alias/utils/SoundPlayer.java @@ -0,0 +1,266 @@ +package me.trouper.alias.utils; + +import me.trouper.alias.server.Main; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitRunnable; + +public class SoundPlayer implements Main { + + private Location location; + private Sound sound; + private float volume; + private float pitch; + + /** + * Constructs a new sound, this aims to add more methods to + * the Bukkit APIs Sound class, as they don't have many + * methods to use. + * + * @param location Location + * @param sound Sound + * @param volume float + * @param pitch float + */ + public SoundPlayer(Location location, Sound sound, float volume, float pitch) { + this.location = location; + this.sound = sound; + this.pitch = pitch; + this.volume = volume; + } + + + /** + * Plays a sound to a player but at the store location + * + * @param player Player + */ + public void play(Player player) { + player.playSound(this.location,this.sound,this.volume,this.pitch); + } + + /** + * Plays a sound to a player but at the player's location + * + * @param player Player + */ + public void playIndividually(Player player) { + player.playSound(player.getLocation(),this.sound,this.volume,this.pitch); + } + + /** + * Plays the sound to all players within a distance, but at the stored location. + * + * @param distance double + */ + public void playWithin(double distance) { + for (Player p : Bukkit.getOnlinePlayers()) { + if (p != null && p.getWorld() == this.location.getWorld() && p.getLocation().distance(this.location) < distance) { + p.playSound(this.location,this.sound,this.volume,this.pitch); + } + } + } + + /** + * Plays the sound to all players within a distance, but at the players' location. + * + * @param distance double + */ + public void playWithinIndividually(double distance) { + for (Player p : Bukkit.getOnlinePlayers()) { + if (p != null && p.getWorld() == this.location.getWorld() && p.getLocation().distance(this.location) < distance) { + p.playSound(p.getLocation(),this.sound,this.volume,this.pitch); + } + } + } + + + /** + * Plays the sound to all players on the server, but at the stored location. + */ + public void playAll() { + for (Player p : Bukkit.getOnlinePlayers()) p.playSound(this.location,this.sound,this.volume,this.pitch); + } + + /** + * Plays the sound to all players on the server, but at the players' location. + */ + public void playAllIndividually() { + for (Player p : Bukkit.getOnlinePlayers()) p.playSound(p.getLocation(),this.sound,this.volume,this.pitch); + } + + /** + * Repeats a sound to a player, but at the stored location. + * + * @param player Player + * @param times int + * @param tickDelay int + */ + public void repeat(Player player, int times, int tickDelay) { + new BukkitRunnable() { + int i = 0; + @Override + public void run() { + if (i < times) { + play(player); + i ++; + } else { + this.cancel(); + } + } + }.runTaskTimer(getPlugin(),0,tickDelay); + } + + /** + * Repeats a sound to a player, but at the player's location. + * + * @param player Player + * @param times int + * @param tickDelay int + */ + public void repeatIndividually(Player player, int times, int tickDelay) { + new BukkitRunnable() { + int i = 0; + @Override + public void run() { + if (i < times) { + playIndividually(player); + i ++; + } else { + this.cancel(); + } + } + }.runTaskTimer(getPlugin(),0,tickDelay); + } + + /** + * Repeats a sound to all players on the server, but at the stored location. + * + * @param times int + * @param tickDelay int + */ + public void repeatAll(int times, int tickDelay) { + new BukkitRunnable() { + int i = 0; + @Override + public void run() { + if (i < times) { + playAll(); + i ++; + } else { + this.cancel(); + } + } + }.runTaskTimer(getPlugin(),0,tickDelay); + } + + /** + * Repeats a sound to all players on the server, but at the players' location. + * + * @param times int + * @param tickDelay int + */ + public void repeatAllIndividually(int times, int tickDelay) { + new BukkitRunnable() { + int i = 0; + @Override + public void run() { + if (i < times) { + playAllIndividually(); + i ++; + } else { + this.cancel(); + } + } + }.runTaskTimer(getPlugin(),0,tickDelay); + } + + /** + * Repeats a sound to all players within a radius, but at the stored location. + * + * @param radius double + * @param times int + * @param tickDelay int + */ + public void repeatAll(double radius,int times, int tickDelay) { + new BukkitRunnable() { + int i = 0; + @Override + public void run() { + if (i < times) { + playWithin(radius); + i ++; + } else { + this.cancel(); + } + } + }.runTaskTimer(getPlugin(),0,tickDelay); + } + + /** + * Repeats a sound to all players within a radius, but at the players' location. + * + * @param distance double + * @param times int + * @param tickDelay int + */ + public void repeatAllIndividually(double distance, int times, int tickDelay) { + new BukkitRunnable() { + int i = 0; + @Override + public void run() { + if (i < times) { + playWithinIndividually(distance); + i ++; + } else { + this.cancel(); + } + } + }.runTaskTimer(getPlugin(),0,tickDelay); + } + + public Sound getSound() { + return sound; + } + + public float getPitch() { + return pitch; + } + + public float getVolume() { + return volume; + } + + public Location getLocation() { + return location; + } + + public void setPitch(float pitch) { + this.pitch = pitch; + } + + public void setVolume(float volume) { + this.volume = volume; + } + + public void setSound(Sound sound) { + this.sound = sound; + } + + public void setLocation(Location location) { + this.location = location; + } + + public void changePlayer(Location location, Sound sound, float volume, float pitch) { + this.location = location; + this.sound = sound; + this.volume = volume; + this.pitch = pitch; + } + + public void changePlayer(Sound sound, float volume, float pitch) { + changePlayer(location, sound, volume, pitch); + } +} diff --git a/src/main/java/me/trouper/alias/utils/TargetingUtils.java b/src/main/java/me/trouper/alias/utils/TargetingUtils.java new file mode 100755 index 0000000..9774464 --- /dev/null +++ b/src/main/java/me/trouper/alias/utils/TargetingUtils.java @@ -0,0 +1,282 @@ +package me.trouper.alias.utils; + +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.bukkit.util.Vector; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +public class TargetingUtils { + + /** + * Applies an action to all living entities within a specified cuboid area that match a given filter. + * + * @param center The center of the cuboid area. + * @param xRadius The half-length of the cuboid along the X-axis. + * @param yRadius The half-length of the cuboid along the Y-axis. + * @param zRadius The half-length of the cuboid along the Z-axis. + * @param filter A predicate to filter which living entities are affected. + * @param action An action to perform on each targeted living entity. + * @return true if any targets were found, false if it missed. + */ + public static boolean areaAffect(Location center, double xRadius, double yRadius, double zRadius, Predicate filter, Consumer action) { + World world = center.getWorld(); + if (world == null) { + return false; + } + + List targets = new ArrayList<>(world.getNearbyLivingEntities(center, xRadius, yRadius, zRadius).stream().filter(filter).toList()); + targets.forEach(action); + return !targets.isEmpty(); + } + + /** + * Applies an action to all living entities within a specified spherical area (approximated by a cube) that match a given filter. + * + * @param center The center of the area. + * @param radius The radius of the spherical area. + * @param filter A predicate to filter which living entities are affected. + * @param action An action to perform on each targeted living entity. + * @return true if any targets were found, false if it missed. + */ + public static boolean areaAffect(Location center, double radius, Predicate filter, Consumer action) { + + World world = center.getWorld(); + if (world == null) { + return false; + } + + List targets = new ArrayList<>(world.getNearbyLivingEntities(center, radius).stream() + .filter(livingEntity -> livingEntity.getLocation().distanceSquared(center) <= radius*radius) + .filter(filter) + .toList()); + targets.forEach(action); + + return !targets.isEmpty(); + } + + /** + * Attempts to find a passable ground location based on the given initial location. + *

+ * This method checks if the current block is non-passable and searches upward (up to 10 blocks) + * until it finds a passable block. Then, it searches downward (up to 25 blocks) for the first + * non-passable block beneath it, which would be considered solid ground. + *

+ * If a valid ground location cannot be determined within the search bounds, + * the original location is returned. + * + * @param initialLocation The starting location to begin the ground-finding process. Must not be null. + * @return A new {@link Location} object representing the nearest solid ground position, + * or the original location if no valid position is found or world is null. + */ + public static Location findGroundLocation(Location initialLocation) { + if (initialLocation == null) { + return null; + } + World world = initialLocation.getWorld(); + if (world == null) return initialLocation; + + Location loc = initialLocation.clone(); + + int attempts = 0; + while (!world.getBlockAt(loc).isPassable() && attempts < 10) { + loc.add(0, 1, 0); + attempts++; + if (loc.getBlockY() >= world.getMaxHeight()) { + return initialLocation; + } + } + if (!world.getBlockAt(loc).isPassable()) return initialLocation; + + attempts = 0; + while (world.getBlockAt(loc).isPassable() && attempts < 25) { + Location belowLoc = loc.clone().subtract(0, 1, 0); + if (belowLoc.getBlockY() <= world.getMinHeight()) { + return world.getBlockAt(belowLoc).isPassable() ? initialLocation : loc; + } + if (!world.getBlockAt(belowLoc).isPassable()) { + return loc; + } + loc.subtract(0, 1, 0); + attempts++; + } + return initialLocation; + } + + /** + * Finds the closest living entity to a central location within a maximum distance. + * + * @param center The central location from which to search. Must not be null. + * @param maxDistance The maximum distance (radius of a sphere) to search for entities. + * @return An {@link Optional} containing the closest {@link LivingEntity}, or an empty Optional if no entity is found or world is null. + */ + public static Optional getClosestLivingEntity(Location center, double maxDistance) { + return getClosestLivingEntity(center, maxDistance, entity -> true); + } + + /** + * Finds the closest living entity to a central location within a maximum distance, matching a given filter. + * + * @param center The central location from which to search. Must not be null. + * @param maxDistance The maximum distance (radius of a sphere) to search for entities. + * @param filter A predicate to apply additional filtering criteria to the entities. Must not be null. + * @return An {@link Optional} containing the closest {@link LivingEntity} matching the criteria, + * or an empty Optional if no such entity is found or world is null. + */ + public static Optional getClosestLivingEntity(Location center, double maxDistance, Predicate filter) { + if (center == null || center.getWorld() == null || filter == null) { + return Optional.empty(); + } + World world = center.getWorld(); + double maxDistanceSquared = maxDistance * maxDistance; + + return world.getNearbyLivingEntities(center, maxDistance, maxDistance, maxDistance, filter).stream() + .filter(entity -> entity.getLocation().distanceSquared(center) <= maxDistanceSquared) + .min(Comparator.comparingDouble(entity -> entity.getLocation().distanceSquared(center))); + } + + /** + * Finds the closest player to a central location within a maximum distance. + * + * @param center The central location from which to search. Must not be null. + * @param maxDistance The maximum distance (radius of a sphere) to search for players. + * @return An {@link Optional} containing the closest {@link Player}, or an empty Optional if no player is found or world is null. + */ + public static Optional getClosestPlayer(Location center, double maxDistance) { + return getClosestPlayer(center, maxDistance, player -> true); + } + + /** + * Finds the closest player to a central location within a maximum distance, matching a given filter. + * + * @param center The central location from which to search. Must not be null. + * @param maxDistance The maximum distance (radius of a sphere) to search for players. + * @param filter A predicate to apply additional filtering criteria to the players. Must not be null. + * @return An {@link Optional} containing the closest {@link Player} matching the criteria, + * or an empty Optional if no such player is found or world is null. + */ + public static Optional getClosestPlayer(Location center, double maxDistance, Predicate filter) { + if (center == null || center.getWorld() == null || filter == null) { + return Optional.empty(); + } + World world = center.getWorld(); + double maxDistanceSquared = maxDistance * maxDistance; + + List nearbyPlayers = world.getPlayers().stream() + .filter(player -> player.getWorld().equals(world)) + .filter(player -> player.getLocation().distanceSquared(center) <= maxDistanceSquared) + .filter(filter) + .collect(Collectors.toList()); + + return nearbyPlayers.stream() + .min(Comparator.comparingDouble(player -> player.getLocation().distanceSquared(center))); + } + + /** + * Finds the living entity with the lowest health within a given radius of a central location. + * + * @param center The central location from which to search. Must not be null. + * @param radius The radius (sphere) to search for entities. + * @return An {@link Optional} containing the {@link LivingEntity} with the lowest health, + * or an empty Optional if no entity is found or world is null. + */ + public static Optional getLowestHealthLivingEntity(Location center, double radius) { + return getLowestHealthLivingEntity(center, radius, entity -> true); + } + + /** + * Finds the living entity with the lowest health within a given radius of a central location, matching a given filter. + * + * @param center The central location from which to search. Must not be null. + * @param radius The radius (sphere) to search for entities. + * @param filter A predicate to apply additional filtering criteria. Must not be null. + * @return An {@link Optional} containing the {@link LivingEntity} with the lowest health matching the criteria, + * or an empty Optional if no such entity is found or world is null. + */ + public static Optional getLowestHealthLivingEntity(Location center, double radius, Predicate filter) { + if (center == null || center.getWorld() == null || filter == null) { + return Optional.empty(); + } + World world = center.getWorld(); + double radiusSquared = radius * radius; + + return world.getNearbyLivingEntities(center, radius, radius, radius, filter).stream() + .filter(entity -> entity.getLocation().distanceSquared(center) <= radiusSquared) + .min(Comparator.comparingDouble(LivingEntity::getHealth)); + } + + /** + * Finds the living entity whose eye location is closest (by angle) to a given direction vector from an origin. + * This method is useful for simulating line-of-sight or aim-based targeting. + * + * @param originEyeLocation The starting location (e.g., an entity's eye location). Must not be null. + * @param direction The normalized direction vector of the search. Must not be null. + * @param maxDistance The maximum distance to search for entities. + * @param maxAngleRadians The maximum allowed angle (in radians) between the direction vector and the vector to the target's eye location. + * A smaller angle means the target is more directly in the line of sight. + * @return An {@link Optional} containing the {@link LivingEntity} closest to the aim vector, + * or an empty Optional if no suitable entity is found or world is null. + */ + public static Optional getLivingEntityClosestToVector(Location originEyeLocation, Vector direction, double maxDistance, double maxAngleRadians) { + return getLivingEntityClosestToVector(originEyeLocation, direction, maxDistance, maxAngleRadians, entity -> true); + } + + /** + * Finds the living entity whose eye location is closest (by angle) to a given direction vector from an origin, matching a filter. + * This method is useful for simulating line-of-sight or aim-based targeting. + * + * @param originEyeLocation The starting location (e.g., an entity's eye location). Must not be null. + * @param direction The normalized direction vector of the search. Must not be null. Its magnitude does not matter as it will be normalized. + * @param maxDistance The maximum distance to search for entities. + * @param maxAngleRadians The maximum allowed angle (in radians) between the direction vector and the vector to the target's eye location. + * A smaller angle means the target is more directly in the line of sight. + * @param filter A predicate to apply additional filtering criteria. Must not be null. + * @return An {@link Optional} containing the {@link LivingEntity} closest to the aim vector and matching the filter, + * or an empty Optional if no suitable entity is found or world is null. + */ + public static Optional getLivingEntityClosestToVector(Location originEyeLocation, Vector direction, double maxDistance, double maxAngleRadians, Predicate filter) { + if (originEyeLocation == null || originEyeLocation.getWorld() == null || direction == null || filter == null) { + return Optional.empty(); + } + World world = originEyeLocation.getWorld(); + Vector normalizedDirection = direction.clone().normalize(); + + List candidates = world.getNearbyLivingEntities(originEyeLocation, maxDistance, maxDistance, maxDistance, entity -> { + if (originEyeLocation.equals(entity.getEyeLocation())) { + return false; + } + return filter.test(entity); + }).stream().toList(); + + LivingEntity bestTarget = null; + double smallestAngle = maxAngleRadians + 1.0; + + for (LivingEntity entity : candidates) { + if (entity.getEyeLocation().distanceSquared(originEyeLocation) > maxDistance * maxDistance) { + continue; + } + + Vector vectorToTarget = entity.getEyeLocation().toVector().subtract(originEyeLocation.toVector()); + if (vectorToTarget.lengthSquared() == 0) { + continue; + } + vectorToTarget.normalize(); + + double angle = normalizedDirection.angle(vectorToTarget); + + if (angle <= maxAngleRadians && angle < smallestAngle) { + smallestAngle = angle; + bestTarget = entity; + } + } + return Optional.ofNullable(bestTarget); + } +} diff --git a/src/main/java/me/trouper/alias/utils/Text.java b/src/main/java/me/trouper/alias/utils/Text.java new file mode 100755 index 0000000..897b319 --- /dev/null +++ b/src/main/java/me/trouper/alias/utils/Text.java @@ -0,0 +1,312 @@ +package me.trouper.alias.utils; + +import me.trouper.alias.server.Main; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.format.TextDecoration; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.Sound; +import org.bukkit.SoundCategory; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class Text implements Main { + + public static String legacyColor(String msg) { + return msg.replaceAll("&","§"); + } + + public static Component color(String msg) { + return LegacyComponentSerializer.legacyAmpersand().deserialize(msg); + } + + public static void sendWarning(CommandSender sender, String warning, Object... args) { + sendMessage(Pallet.WARNING, sender, warning, args); + } + + public static void sendError(CommandSender sender, String error, Object... args) { + sendMessage(Pallet.ERROR, sender, error, args); + } + + public static void sendMessage(CommandSender sender, String text, Object... args) { + sendMessage(Pallet.NEUTRAL, sender, text, args); + } + + public static void sendMessage(Pallet pallet, CommandSender sender, String text, Object... args) { + text = formatArgsLegacy(pallet, text, args); + sendMessage(sender, text); + if (sender instanceof Player p) p.playSound(p.getLocation(),pallet.sound.sound, SoundCategory.VOICE,10f,pallet.sound.pitch); + } + + public static void sendMessage(CommandSender sender, String text) { + Component message = getMessage(text); + sender.sendMessage(message); + } + + public static Component getMessage(Pallet pallet, String text, Object... args) { + return getMessage(formatArgsLegacy(pallet, text, args)); + } + + public static Component getMessage(String text) { + if (main.config().messages.fancyAlerts) { + return formatFancyMessage(text); + } else { + return color(main.config().messages.prefix + text); + } + } + + public static String formatArgsLegacy(Pallet pallet, String format, Object... args) { + return LegacyComponentSerializer.legacyAmpersand().serialize(formatArgs(pallet,format,args)); + } + + public static Component formatArgs(Pallet pallet, String format, Object... args) { + Component message = Component.empty(); + Pattern pattern = Pattern.compile("\\{(\\d+)}"); + Matcher matcher = pattern.matcher(format); + int lastIndex = 0; + + while (matcher.find()) { + String prefix = format.substring(lastIndex, matcher.start()); + if (!prefix.isEmpty()) { + message = message.append(Component.text(prefix).color(pallet.mainText)); + } + + int argIndex = Integer.parseInt(matcher.group(1)); + TextColor argColor = getArgColor(pallet, argIndex); + + if (argIndex >= 0 && argIndex < args.length) { + String argText = args[argIndex].toString(); + message = message.append(Component.text(argText).color(argColor)); + } else { + message = message.append(Component.text(matcher.group()).color(pallet.mainText)); + } + + lastIndex = matcher.end(); + } + + String suffix = format.substring(lastIndex); + if (!suffix.isEmpty()) { + message = message.append(Component.text(suffix).color(pallet.mainText)); + } + + return message; + } + + public static Component formatFancyMessage(String text) { + Component message = Component.empty().appendNewline(); + + List wrappedLines = wrapText(text, 50, (int) Math.round((main.config().messages.pluginName.length() + 3) * 1.3)); + + message = message + .append(color(main.config().messages.mainColor + "| ").decorate(TextDecoration.BOLD)) + .append(Component.text(main.config().messages.pluginName + " ", NamedTextColor.WHITE, TextDecoration.BOLD)) + .append(color(wrappedLines.getFirst())); + + String active = getActiveFormatting(wrappedLines.getFirst()); + + wrappedLines.removeFirst(); + + for (String wrappedLine : wrappedLines) { + wrappedLine = active + wrappedLine; + + active = getActiveFormatting(wrappedLine); + message = message + .appendNewline() + .append(color(main.config().messages.mainColor + "| ").decorate(TextDecoration.BOLD)) + .append(color(wrappedLine)); + } + + return message.appendNewline(); + } + + public static List wrapText(String text, int maxLineLength, int offset) { + List lines = new ArrayList<>(); + + if (text == null || text.isEmpty() || maxLineLength <= 0) { + return lines; + } + + String[] words = text.split("\\s+"); + StringBuilder currentLine = new StringBuilder(); + int currentLineLength = offset; + + for (String word : words) { + if (currentLineLength + word.length() + 1 > maxLineLength) { + lines.add(currentLine.toString()); + currentLine = new StringBuilder(); + currentLineLength = 0; + } + + if (!currentLine.isEmpty()) { + currentLine.append(" "); + currentLineLength++; + } + + currentLine.append(word); + currentLineLength += word.length(); + } + + if (!currentLine.isEmpty()) { + lines.add(currentLine.toString()); + } + + return lines; + } + + public static String getActiveFormatting(String text) { + final Pattern pattern = Pattern.compile("&[0-9a-fk-or]"); + final Matcher matcher = pattern.matcher(text); + + String lastColor = ""; + Set activeFormats = new HashSet<>(); + + while (matcher.find()) { + String code = matcher.group(); + char identifier = code.charAt(1); + + if (identifier >= '0' && identifier <= '9' || identifier >= 'a' && identifier <= 'f') { + lastColor = code; + activeFormats.clear(); + } else if (identifier >= 'k' && identifier <= 'o') { + activeFormats.add(identifier); + } else if (identifier == 'r') { + lastColor = ""; + activeFormats.clear(); + } + } + + StringBuilder result = new StringBuilder(lastColor); + for (char format : activeFormats) { + result.append("&").append(format); + } + + return result.toString(); + } + + private static TextColor getArgColor(Pallet pallet, int argIndex) { + return switch (argIndex) { + case 1 -> pallet.arg2; + case 2 -> pallet.arg3; + default -> pallet.argDefault; + }; + } + + public enum Pallet { + ERROR( + NamedTextColor.RED, + NamedTextColor.YELLOW, + NamedTextColor.GOLD, + NamedTextColor.DARK_RED, + new SoundData(Sound.BLOCK_NOTE_BLOCK_BASS,1)), + WARNING( + NamedTextColor.YELLOW, + NamedTextColor.GOLD, + NamedTextColor.RED, + NamedTextColor.DARK_RED, + new SoundData(Sound.BLOCK_NOTE_BLOCK_BIT,0.5F)), + INFO( + NamedTextColor.GRAY, + NamedTextColor.WHITE, + NamedTextColor.AQUA, + NamedTextColor.DARK_AQUA, + new SoundData(Sound.BLOCK_NOTE_BLOCK_BELL,1)), + SUCCESS( + NamedTextColor.GREEN, + NamedTextColor.DARK_GREEN, + NamedTextColor.YELLOW, + NamedTextColor.GOLD, + new SoundData(Sound.BLOCK_NOTE_BLOCK_CHIME,1)), + NEUTRAL( + NamedTextColor.GRAY, + NamedTextColor.WHITE, + NamedTextColor.DARK_AQUA, + NamedTextColor.BLUE, + new SoundData(Sound.BLOCK_NOTE_BLOCK_BELL,1)); + + private final TextColor mainText; + private final TextColor argDefault; + private final TextColor arg2; + private final TextColor arg3; + private final SoundData sound; + + Pallet(TextColor mainText, TextColor argDefault, TextColor arg2, TextColor arg3, SoundData sound) { + this.mainText = mainText; + this.argDefault = argDefault; + this.arg2 = arg2; + this.arg3 = arg3; + this.sound = sound; + } + } + + public record SoundData(Sound sound, float pitch){}; + + public static String generateProgressBar(int length, int max, int current) { + if (max <= 0) { + throw new IllegalArgumentException("Max value must be greater than 0"); + } + + current = Math.max(0, Math.min(current, max)); + double percent = (double) current / max; + int filledBars = (int) Math.round(percent * length); + + StringBuilder progressBar = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i < filledBars) { + progressBar.append("&a|"); + } else { + progressBar.append("&7|"); + } + } + + return progressBar.toString(); + } + + public static String formatEnum(Enum obj) { + if (obj == null) return "Null"; + String name = obj.name(); + String[] words = name.toLowerCase().split("_"); + + StringBuilder formatted = new StringBuilder(); + + for (String word : words) { + if (!word.isEmpty()) { + formatted.append(Character.toUpperCase(word.charAt(0))) + .append(word.substring(1)) + .append(" "); + } + } + + return formatted.toString().trim(); + } + + public static int getLevel(String s) { + HashMap romanToInt = new HashMap(); + romanToInt.put(Character.valueOf('I'), 1); + romanToInt.put(Character.valueOf('V'), 5); + romanToInt.put(Character.valueOf('X'), 10); + romanToInt.put(Character.valueOf('L'), 50); + romanToInt.put(Character.valueOf('C'), 100); + romanToInt.put(Character.valueOf('D'), 500); + romanToInt.put(Character.valueOf('M'), 1000); + int result = 0; + for (int i = 0; i < s.length(); ++i) { + if (i > 0 && romanToInt.get(Character.valueOf(s.charAt(i))) > romanToInt.get(Character.valueOf(s.charAt(i - 1)))) { + result += romanToInt.get(Character.valueOf(s.charAt(i))) - 2 * romanToInt.get(Character.valueOf(s.charAt(i - 1))); + continue; + } + if (romanToInt.get(Character.valueOf(s.charAt(i))) == null) { + return 0; + } + result += (romanToInt.get(Character.valueOf(s.charAt(i)))).intValue(); + } + if (result > 255) { + result = 1; + } + return result; + } +} \ No newline at end of file diff --git a/src/main/java/me/trouper/alias/utils/Verbose.java b/src/main/java/me/trouper/alias/utils/Verbose.java new file mode 100755 index 0000000..f1f90e7 --- /dev/null +++ b/src/main/java/me/trouper/alias/utils/Verbose.java @@ -0,0 +1,70 @@ +package me.trouper.alias.utils; + +import me.trouper.alias.server.Main; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +public class Verbose implements Main { + + public static void send(int backtrace, String message, Object... args) { + if (!me.trouper.alias.Alias.getInstance().getManager().io.config.debugMode) return; + String callerInfo = "Unknown Caller"; + + StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + if (stackTrace.length > 2 + backtrace) { + StackTraceElement caller = stackTrace[2 + backtrace]; + + String className = caller.getClassName(); + className = className.substring(className.lastIndexOf(".") + 1); + if (className.contains("-")) { + callerInfo = "Protected"; + } else { + callerInfo = className + "." + caller.getMethodName(); + } + + if (me.trouper.alias.Alias.getInstance().getManager().io.config.debuggerExclusions.contains(callerInfo)) { + return; + } + } + + String formattedMessage = message.formatted(args); + String log = "[DEBUG ^ %s] [%s]: %s".formatted(backtrace, callerInfo, formattedMessage); + me.trouper.alias.Alias.getInstance().getLogger().info(log); + + for (Player operator : Bukkit.getOnlinePlayers()) { + if (operator.isOp()) operator.sendMessage("§d§l%s §7[§bDEBUG ^ %s§7] §7[§e%s§7] §8» §7%s" + .formatted(main.config().messages.pluginName,backtrace, callerInfo, formattedMessage)); + } + } + + public static void send(String message, Object... args) { + if (!me.trouper.alias.Alias.getInstance().getManager().io.config.debugMode) return; + String callerInfo = "Unknown Caller"; + + StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + if (stackTrace.length > 2) { + StackTraceElement caller = stackTrace[2]; + + String className = caller.getClassName(); + className = className.substring(className.lastIndexOf(".") + 1); + if (className.contains("-")) { + callerInfo = "Protected"; + } else { + callerInfo = className + "." + caller.getMethodName(); + } + + if (main.config().debuggerExclusions.contains(callerInfo)) { + return; + } + } + + String formattedMessage = message.formatted(args); + String log = "[DEBUG] [%s]: %s".formatted(callerInfo, formattedMessage); + me.trouper.alias.Alias.getInstance().getLogger().info(log); + + for (Player operator : Bukkit.getOnlinePlayers()) { + if (operator.isOp()) operator.sendMessage("§d§l%s §7[§bDEBUG§7] §7[§e%s§7] §8» §7%s" + .formatted(main.config().messages.pluginName,callerInfo, formattedMessage)); + } + } +} diff --git a/src/main/java/me/trouper/alias/utils/command/Args.java b/src/main/java/me/trouper/alias/utils/command/Args.java new file mode 100755 index 0000000..d533511 --- /dev/null +++ b/src/main/java/me/trouper/alias/utils/command/Args.java @@ -0,0 +1,115 @@ +package me.trouper.alias.utils.command; + +import java.util.function.Consumer; + +public record Args(String... args) { + + public Arg getAll() { + return getAll(0); + } + + public Arg getAll(int beginIndex) { + String str = ""; + for (int i = beginIndex; i < args.length; i++) { + str = str.concat(args[i] + " "); + } + return new Arg(str.trim()); + } + + public Arg get(int index) { + if (args.length == 0) + throw new IllegalArgumentException("not enough arguments: arguments are empty"); + if (index < 0 || index >= args.length) + throw new IllegalArgumentException("not enough arguments: argument %s is missing".formatted(index + 1)); + return new Arg(args[index]); + } + + public Arg first() { + return get(0); + } + + public Arg last() { + return get(args.length - 1); + } + + public boolean match(int index, String arg) { + if (index < 0 || index >= args.length) { + return false; + } + return get(index).toString().equalsIgnoreCase(arg); + } + + public void when(int index, String match, Consumer action) { + if (match(index, match)) { + action.accept(get(index)); + } + } + + public int getSize() { + return args.length; + } + + public boolean isEmpty() { + return args.length == 0; + } + + public static class Arg { + private final String arg; + + public Arg(String arg) { + this.arg = arg; + } + + public int toInt() { + return Integer.parseInt(arg); + } + + public long toLong() { + return Long.parseLong(arg); + } + + public byte toByte() { + return Byte.parseByte(arg); + } + + public short toShort() { + return Short.parseShort(arg); + } + + public double toDouble() { + return Double.parseDouble(arg); + } + + public float toFloat() { + return Float.parseFloat(arg); + } + + public boolean toBool() { + return Boolean.parseBoolean(arg); + } + + public char toChar() { + return arg.isEmpty() ? ' ' : arg.charAt(0); + } + + @Override + public String toString() { + return arg; + } + + public > T toEnum(Class enumType) { + return toEnum(enumType, null); + } + + public > T toEnum(Class enumType, T fallback) { + String arg = this.arg.replace('-', '_'); + for (T constant : enumType.getEnumConstants()) + if (arg.equalsIgnoreCase(constant.name())) + return constant; + + if (fallback == null) + throw new IllegalArgumentException("'%s' is not a value of %s".formatted(arg, enumType.getSimpleName())); + return fallback; + } + } +} \ No newline at end of file diff --git a/src/main/java/me/trouper/alias/utils/command/CommandRegistry.java b/src/main/java/me/trouper/alias/utils/command/CommandRegistry.java new file mode 100755 index 0000000..f19dd89 --- /dev/null +++ b/src/main/java/me/trouper/alias/utils/command/CommandRegistry.java @@ -0,0 +1,17 @@ +package me.trouper.alias.utils.command; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface CommandRegistry { + + String value(); + String usage() default "none"; + Permission permission() default @Permission(""); + boolean printStackTrace() default false; + boolean playersOnly() default false; +} diff --git a/src/main/java/me/trouper/alias/utils/command/Permission.java b/src/main/java/me/trouper/alias/utils/command/Permission.java new file mode 100755 index 0000000..b7446e4 --- /dev/null +++ b/src/main/java/me/trouper/alias/utils/command/Permission.java @@ -0,0 +1,14 @@ +package me.trouper.alias.utils.command; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface Permission { + + String value(); + String message() default "&cYou do not have permission for this command!"; +} diff --git a/src/main/java/me/trouper/alias/utils/command/QuickCommand.java b/src/main/java/me/trouper/alias/utils/command/QuickCommand.java new file mode 100755 index 0000000..f3abcae --- /dev/null +++ b/src/main/java/me/trouper/alias/utils/command/QuickCommand.java @@ -0,0 +1,99 @@ +package me.trouper.alias.utils.command; + +import me.trouper.alias.server.Main; +import me.trouper.alias.utils.command.completions.CompletionBuilder; +import me.trouper.alias.utils.command.completions.CompletionNode; +import me.trouper.alias.utils.misc.Voidable; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.command.PluginCommand; +import org.bukkit.command.TabExecutor; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.List; + +public interface QuickCommand extends TabExecutor, Main { + + void dispatchCommand(CommandSender sender, Command command, String label, Args args); + + void dispatchCompletions(CommandSender sender, Command command, String label, CompletionBuilder b); + + default void register() { + CommandRegistry registry = this.getClass().getAnnotation(CommandRegistry.class); + PluginCommand command = getPlugin().getCommand(registry.value()); + + if (command != null) { + command.setExecutor(this); + command.setTabCompleter(this); + } + } + + @Override + default boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + CommandRegistry registry = this.getClass().getAnnotation(CommandRegistry.class); + if (registry == null) { + return true; + } + if (!(sender instanceof Player) && registry.playersOnly()) { + info(sender, "This command is for players only!"); + return true; + } + + try { + String perm = registry.permission().value(); + if (perm != null && !perm.isEmpty() && !sender.hasPermission(perm)) { + error(sender, registry.permission().message()); + return true; + } + dispatchCommand(sender, command, label, new Args(args)); + } + catch (Exception ex) { + if (registry.printStackTrace()) { + ex.printStackTrace(); + } + info(sender, "&cCorrect Usage: &7" + registry.usage()); + } + return true; + } + + @Override + default List onTabComplete(CommandSender sender, Command command, String label, String[] args) { + try { + CompletionBuilder b = new CompletionBuilder(label); + dispatchCompletions(sender, command, label, b); + CompletionNode node = b.getRootNode(); + + if (args.length == 0) { + return node.getOptions(); + } + for (int i = 0; i < args.length - 1; i++) { + node = node.next(args[i]); + } + + String end = args[args.length - 1]; + List a = new ArrayList<>(node.getOptions()); + + if (node.isOptionsRegex()) { + List regexResult = new ArrayList<>(); + for (CompletionNode option : node.getNextOptions()) { + boolean regexMatches = CompletionNode.containsRegex(option, end) || end.isEmpty(); + for (String s : option.getValues()) + regexResult.add((regexMatches ? "§d" : "§c") + s + "§r"); + } + return regexResult; + } + else { + a.removeIf(s -> !s.toLowerCase().contains(end.toLowerCase())); + return a; + } + } + catch (Exception ex) { + return new ArrayList<>(); + } + } + + default Voidable getRegistry() { + return Voidable.of(this.getClass().getAnnotation(CommandRegistry.class)); + } +} diff --git a/src/main/java/me/trouper/alias/utils/command/completions/CompletionBuilder.java b/src/main/java/me/trouper/alias/utils/command/completions/CompletionBuilder.java new file mode 100755 index 0000000..2fe7e33 --- /dev/null +++ b/src/main/java/me/trouper/alias/utils/command/completions/CompletionBuilder.java @@ -0,0 +1,123 @@ +package me.trouper.alias.utils.command.completions; + +import me.trouper.alias.utils.misc.ArrayUtils; + +import java.util.*; +import java.util.function.Function; + +public class CompletionBuilder { + + private final CompletionNode root; + private final List options; + private boolean isBranch; + private String regex; + + CompletionBuilder(List names) { + this.root = new CompletionNode(names, new ArrayList<>(), null); + this.options = new ArrayList<>(); + this.isBranch = false; + } + + public CompletionBuilder(String names) { + this.root = new CompletionNode(names); + this.options = new ArrayList<>(); + this.isBranch = false; + } + + public CompletionBuilder(String regex, String details) { + this.root = new CompletionNode(Collections.singletonList(details), new ArrayList<>(), regex); + this.options = new ArrayList<>(); + this.isBranch = false; + this.regex = regex; + } + + public CompletionBuilder then(CompletionBuilder arg) { + options.add(arg); + root.nextOptions.add(arg.root); + return this; + } + + public CompletionBuilder arg(List name) { + CompletionBuilder b = new CompletionBuilder(name); + b.isBranch = true; + return b; + } + + public CompletionBuilder argRegex(String regex, String details) { + CompletionBuilder b = new CompletionBuilder(regex, details); + b.isBranch = true; + return b; + } + + public CompletionBuilder argInt(String details) { + return argRegex("^ *\\-?\\d+ *$", details); + } + + public CompletionBuilder argPosInt(String details) { + return argRegex("^ *\\d+ *$", details); + } + + public CompletionBuilder argDecimal(String details) { + return argRegex("^ *\\-?\\d*\\.?\\d+ *$", details); + } + + public CompletionBuilder argPosDecimal(String details) { + return argRegex("^ *\\d*\\.?\\d+ *$", details); + } + + public CompletionBuilder argBool() { + return arg("true", "false"); + } + + public CompletionBuilder argEnum(Class> type, boolean lowercase) { + return arg(ArrayUtils.enumNames(type, lowercase)); + } + + public CompletionBuilder argEnum(Class> type) { + return argEnum(type, true); + } + + public CompletionBuilder argOnlinePlayers() { + return arg(ArrayUtils.playerNames()); + } + + public CompletionBuilder arg(String... names) { + return arg(Arrays.asList(names)); + } + + public CompletionBuilder arg(String name) { + return arg(Collections.singletonList(name)); + } + + public CompletionBuilder arg(Collection input, Function toString) { + return arg(input.stream().map(toString).toList()); + } + + public CompletionBuilder next(String name) { + for (CompletionBuilder o : options) { + if (CompletionNode.strictContains(o.root, name)) { + return o; + } + } + return null; + } + + public CompletionNode getRootNode() { + return root; + } + + public CompletionNode build() { + if (this.isBranch) { + throw new IllegalArgumentException("build() cannot be called on branches!"); + } + return root; + } + + public boolean isBranch() { + return isBranch; + } + + public boolean isRegex() { + return regex != null; + } +} \ No newline at end of file diff --git a/src/main/java/me/trouper/alias/utils/command/completions/CompletionNode.java b/src/main/java/me/trouper/alias/utils/command/completions/CompletionNode.java new file mode 100755 index 0000000..4b8d238 --- /dev/null +++ b/src/main/java/me/trouper/alias/utils/command/completions/CompletionNode.java @@ -0,0 +1,88 @@ +package me.trouper.alias.utils.command.completions; + +import java.util.ArrayList; +import java.util.List; + +public class CompletionNode { + + final List values; + final List nextOptions; + final String regex; + + CompletionNode(List values, List nextOptions, String regex) { + this.values = values; + this.nextOptions = nextOptions; + this.regex = regex; + } + + CompletionNode(String values) { + this(List.of(values), new ArrayList<>(), null); + } + + public static boolean strictContains(CompletionNode parent, String subject) { + for (String value : parent.values) + if (value.equals(subject)) + return true; + return false; + } + + public static boolean contains(CompletionNode parent, String subject) { + for (String value : parent.values) + if (value.contains(subject)) + return true; + return false; + } + + public static boolean containsRegex(CompletionNode parent, String subject) { + if (parent.regex == null) + return false; + return subject.matches(parent.regex); + } + + public boolean optionsRegexMatchesArg(String argument) { + for (CompletionNode option : nextOptions) + if (containsRegex(option, argument)) + return true; + return false; + } + + public CompletionNode next(String argument) { + for (CompletionNode option : nextOptions) + if (containsRegex(option, argument)) + return option; + + for (CompletionNode option : nextOptions) + if (strictContains(option, argument)) + return option; + + for (CompletionNode option : nextOptions) + if (contains(option, argument)) + return option; + + return null; + } + + public List getOptions() { + List a = new ArrayList<>(); + for (CompletionNode o : nextOptions) { + a.addAll(o.values); + } + return a; + } + + public boolean isRegex() { + return regex != null; + } + + public boolean isOptionsRegex() { + return nextOptions.stream().anyMatch(CompletionNode::isRegex); + } + + public List getNextOptions() { + return nextOptions; + } + + public List getValues() { + return values; + } +} \ No newline at end of file diff --git a/src/main/java/me/trouper/alias/utils/misc/ArrayUtils.java b/src/main/java/me/trouper/alias/utils/misc/ArrayUtils.java new file mode 100755 index 0000000..849c668 --- /dev/null +++ b/src/main/java/me/trouper/alias/utils/misc/ArrayUtils.java @@ -0,0 +1,71 @@ +package me.trouper.alias.utils.misc; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; + +public final class ArrayUtils { + + /** + * Transforms an array to another one + * @param e iterable list + * @param a action + * @return new transformed list + * @param input + * @param output + */ + public static List map(Iterable e, Function a) { + List list = new ArrayList<>(); + e.forEach(i -> list.add(a.apply(i))); + return list; + } + + public static String toPrettyString(List list) { + return "§7[§e" + String.join("§7, §e", ArrayUtils.map(list, Object::toString)) + "§7]"; + } + + public static > List enumNames(Class type, boolean lowercase) { + List names = new ArrayList<>(); + for (E constant : type.getEnumConstants()) { + String name = constant.name(); + names.add(lowercase ? name.toLowerCase() : name); + } + return names; + } + + public static > List enumNames(Class type) { + return enumNames(type, true); + } + + public static List playerNames() { + return map(Bukkit.getOnlinePlayers(), Player::getName); + } + + @SafeVarargs + public static List bind(Iterable tList, T... ts) { + List list = Arrays.asList(ts); + tList.forEach(list::add); + return list; + } + + public static List reversed(List input) { + Collections.reverse(input); + return input; + } + + public static List reversed(Iterable input) { + List list = new ArrayList<>(); + input.forEach(list::add); + return reversed(list); + } + + public static void reverseForEach(Iterable input, Consumer action) { + reversed(input).forEach(action); + } +} diff --git a/src/main/java/me/trouper/alias/utils/misc/Cooldown.java b/src/main/java/me/trouper/alias/utils/misc/Cooldown.java new file mode 100755 index 0000000..391c910 --- /dev/null +++ b/src/main/java/me/trouper/alias/utils/misc/Cooldown.java @@ -0,0 +1,42 @@ +package me.trouper.alias.utils.misc; + +import java.util.HashMap; +import java.util.Map; + +public class Cooldown { + + private final Map timer; + + public Cooldown() { + this.timer = new HashMap<>(); + } + + private O getOrDefault(O value, O def) { + return value != null ? value : def; + } + + public long getCooldown(T obj) { + return Math.max(getOrDefault(timer.get(obj), 0L) - System.currentTimeMillis(), 0L); + } + + public double getCooldownSec(T obj) { + final long cooldown = this.getCooldown(obj); + return Math.floor(cooldown / 10.0) / 100.0; + } + + public boolean isOnCooldown(T obj) { + return getCooldown(obj) > 0L; + } + + public void setCooldown(T obj, long millis) { + timer.put(obj, System.currentTimeMillis() + millis); + } + + public void addCooldown(T obj, long millis) { + setCooldown(obj, getCooldown(obj) + millis); + } + + public void removeCooldown(T obj) { + timer.remove(obj); + } +} diff --git a/src/main/java/me/trouper/alias/utils/misc/FileValidationUtils.java b/src/main/java/me/trouper/alias/utils/misc/FileValidationUtils.java new file mode 100755 index 0000000..177cb19 --- /dev/null +++ b/src/main/java/me/trouper/alias/utils/misc/FileValidationUtils.java @@ -0,0 +1,21 @@ +package me.trouper.alias.utils.misc; + +import java.io.File; + +public final class FileValidationUtils { + + public static boolean validate(File file) { + try { + if (!file.getParentFile().exists()) + if (!file.getParentFile().mkdirs()) + return false; + if (!file.exists()) + if (!file.createNewFile()) + return false; + return true; + } + catch (Exception ex) { + return false; + } + } +} diff --git a/src/main/java/me/trouper/alias/utils/misc/JsonSerializable.java b/src/main/java/me/trouper/alias/utils/misc/JsonSerializable.java new file mode 100755 index 0000000..96ca146 --- /dev/null +++ b/src/main/java/me/trouper/alias/utils/misc/JsonSerializable.java @@ -0,0 +1,149 @@ +package me.trouper.alias.utils.misc; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import java.io.*; + +public interface JsonSerializable { + + Gson gson = new GsonBuilder().setPrettyPrinting().serializeNulls().setLenient().create(); + File getFile(); + + default String serialize(boolean pretty) { + Gson gson; + if (pretty) { + gson = new GsonBuilder().setPrettyPrinting().setLenient().create(); + } + else { + gson = new Gson(); + } + + try { + String json = gson.toJson(this); + if (json == null) { + throw new IllegalStateException("json parse failed for " + this.getClass().getSimpleName()); + } + return json; + } + catch (Exception ex) { + return "{}"; + } + } + + @SuppressWarnings("unchecked") + default T deserialize(String json) { + try { + JsonSerializable v = gson.fromJson(json, this.getClass()); + if (v == null) { + throw new IllegalStateException("json parse failed"); + } + return (T)v; + } + catch (Exception ex) { + return null; + } + } + + default JsonObject getJson() { + return gson.toJsonTree(this).getAsJsonObject(); + } + + /** + * Gets a json element given the specified member path + * @param path Path separated by a period . between each member name + * @return the JsonElement at the end of the path, otherwise null + */ + default JsonElement get(String path) { + JsonElement root = gson.toJsonTree(this); + JsonElement json = root; + + for (String memberName : path.split("\\.")) { + JsonElement e = json.getAsJsonObject().get(memberName); + if (e != null) + json = e; + else + break; + } + + return json == root ? null : json; + } + + /** + * Gets a json element given the specified member path + * @param path Path separated by a period . between each member name + */ + default boolean set(String path, Object obj) { + JsonElement root = gson.toJsonTree(this); + JsonElement json = root; + String[] paths = path.split("\\."); + + if (paths.length == 0) + return false; + if (paths.length == 1) { + root.getAsJsonObject().add(path, gson.toJsonTree(obj)); + return true; + } + + for (int i = 0; i < paths.length - 1; i++) { + JsonElement e = json.getAsJsonObject().get(paths[i]); + if (e != null) + json = e; + else + break; + } + + if (json != root) { + json.getAsJsonObject().add(paths[paths.length - 1], gson.toJsonTree(obj)); + return true; + } + return false; + } + + default void save() { + String json = serialize(true); + File f = getFile(); + + if (FileValidationUtils.validate(f)) { + try { + FileWriter fw = new FileWriter(f); + BufferedWriter bw = new BufferedWriter(fw); + bw.write(json); + bw.close(); + } + catch (Exception ex) { + ex.printStackTrace(); + } + } + } + + default O getOrDef(O val, O def) { + return val != null ? val : def; + } + + static > T load(File file, Class jsonSerializable, T fallback) { + if (FileValidationUtils.validate(file)) { + try { + FileReader fr = new FileReader(file); + BufferedReader br = new BufferedReader(fr); + T t = gson.fromJson(br, jsonSerializable); + + if (t == null) { + throw new IllegalStateException("json parse failed!"); + } + + return t; + } + catch (Exception ex) { + ex.printStackTrace(); + } + } + return fallback; + } + + static > T load(String path, Class jsonSerializable, T fallback) { + return load(new File(path), jsonSerializable, fallback); + } +} diff --git a/src/main/java/me/trouper/alias/utils/misc/Randomizer.java b/src/main/java/me/trouper/alias/utils/misc/Randomizer.java new file mode 100755 index 0000000..6beaa66 --- /dev/null +++ b/src/main/java/me/trouper/alias/utils/misc/Randomizer.java @@ -0,0 +1,81 @@ +package me.trouper.alias.utils.misc; + +import java.util.List; + +public class Randomizer { + + public Randomizer() { + + } + + public T getRandomElement(List list) { + if (list == null || list.isEmpty()) { + return null; + } + return list.get(getRandomIndex(list.size())); + } + + @SafeVarargs + public final T getRandomElement(T... list) { + if (list == null || list.length == 0) { + return null; + } + return list[getRandomIndex(list.length)]; + } + + private int getRandomIndex(int listSize) { + if (listSize < 0) { + listSize = 0; + } + return (int)(Math.ceil(Math.random() * listSize) - 1); + } + + public boolean getRandomBoolean() { + return Math.random() < 0.5; + } + + /** + * 'Percentage' means an integer from 0-100. You should not divide this value by 100, as this does it for you. + * @param percentage an integer 0-100 + * @return true if chance hit, false otherwise + */ + public boolean getRandomChance(int percentage) { + return Math.random() < percentage / 100.0; + } + + public int getRandomInt(int min, int max) { + if (min > max) { + throw new IllegalArgumentException("min cannot be greater than max!"); + } + int range = max - min + 1; + return min + (int)(Math.random() * range); + } + + public int getRandomInt(int max) { + return getRandomInt(0, max); + } + + public double getRandomDouble(double min, double max) { + if (min > max) { + throw new IllegalArgumentException("min cannot be greater than max!"); + } + double range = max - min; + return min + Math.random() * range; + } + + public double getRandomDouble(double max) { + return getRandomDouble(0.0, max); + } + + public float getRandomFloat(float min, float max) { + if (min > max) { + throw new IllegalArgumentException("min cannot be greater than max!"); + } + float range = max - min; + return (float)(min + Math.random() * range); + } + + public float getRandomFloat(float max) { + return getRandomFloat(0.0F, max); + } +} \ No newline at end of file diff --git a/src/main/java/me/trouper/alias/utils/misc/Voidable.java b/src/main/java/me/trouper/alias/utils/misc/Voidable.java new file mode 100755 index 0000000..beacdb9 --- /dev/null +++ b/src/main/java/me/trouper/alias/utils/misc/Voidable.java @@ -0,0 +1,59 @@ +package me.trouper.alias.utils.misc; + +import me.trouper.alias.server.Main; + +import java.util.function.Consumer; +import java.util.function.Function; + +public class Voidable implements Main { + + private final T value; + + private Voidable(T value) { + this.value = value; + } + + public T get() { + return value; + } + + public boolean isPresent() { + return value != null; + } + + public Voidable map(Function function) { + return isPresent() ? of(function.apply(value)) : of(null); + } + + public void accept(Consumer action) { + if (isPresent()) { + action.accept(value); + } + } + + public void accept(Consumer action, Runnable orElse) { + if (isPresent()) { + action.accept(value); + } + else { + orElse.run(); + } + } + + public T getOrDef(T fallback) { + return isPresent() ? value : fallback; + } + + public T getOrThrow(String msg, Object... args) { + checkPre(isPresent(), msg, args); + return value; + } + + public T getOrThrow() { + return getOrThrow("value is not present."); + } + + public static Voidable of(T value) { + return new Voidable<>(value); + } +} \ No newline at end of file diff --git a/src/main/java/me/trouper/alias/utils/nbt/PersistentData.java b/src/main/java/me/trouper/alias/utils/nbt/PersistentData.java new file mode 100644 index 0000000..65af718 --- /dev/null +++ b/src/main/java/me/trouper/alias/utils/nbt/PersistentData.java @@ -0,0 +1,59 @@ +package me.trouper.alias.utils.nbt; + +import me.trouper.alias.server.Main; +import org.bukkit.NamespacedKey; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; + +public class PersistentData implements Main { + + private final PersistentDataContainer data; + + public PersistentData(PersistentDataContainer data) { + this.data = data; + } + + public void write(String namespace, PersistentDataSerializable obj) { + if (namespace == null || namespace.isEmpty()) + return; + if (obj == null) { + remove(namespace); + return; + } + + NamespacedKey key = new NamespacedKey(main.getPlugin(), namespace); + String json = obj.serialize(); + + data.set(key, PersistentDataType.STRING, json); + } + + public void remove(String namespace) { + NamespacedKey key = new NamespacedKey(main.getPlugin(), namespace); + data.remove(key); + } + + public T read(String namespace, Class type) { + return read(namespace, type, null); + } + + public T read(String namespace, Class type, T fallback) { + try { + NamespacedKey key = new NamespacedKey(main.getPlugin(), namespace); + String json = data.get(key, PersistentDataType.STRING); + T obj = PersistentDataSerializable.gson.fromJson(json, type); + return obj != null ? obj : fallback; + } + catch (Exception ex) { + return fallback; + } + } + + public boolean valid(String namespace, Class type) { + return read(namespace, type, null) != null; + } + + public boolean has(String namespace) { + NamespacedKey key = new NamespacedKey(main.getPlugin(), namespace); + return data.has(key); + } +} \ No newline at end of file diff --git a/src/main/java/me/trouper/alias/utils/nbt/PersistentDataSerializable.java b/src/main/java/me/trouper/alias/utils/nbt/PersistentDataSerializable.java new file mode 100644 index 0000000..7f9f8f2 --- /dev/null +++ b/src/main/java/me/trouper/alias/utils/nbt/PersistentDataSerializable.java @@ -0,0 +1,17 @@ +package me.trouper.alias.utils.nbt; + +import com.google.gson.Gson; + +public interface PersistentDataSerializable { + + Gson gson = new Gson(); + + default String serialize() { + try { + return gson.toJson(this); + } + catch (Exception ex) { + return "{}"; + } + } +} \ No newline at end of file diff --git a/src/main/java/me/trouper/alias/utils/visual/BlockDisplayRaytracer.java b/src/main/java/me/trouper/alias/utils/visual/BlockDisplayRaytracer.java new file mode 100755 index 0000000..d156cb2 --- /dev/null +++ b/src/main/java/me/trouper/alias/utils/visual/BlockDisplayRaytracer.java @@ -0,0 +1,351 @@ +package me.trouper.alias.utils.visual; + +import me.trouper.alias.server.Main; +import org.bukkit.*; +import org.bukkit.block.Block; +import org.bukkit.entity.BlockDisplay; +import org.bukkit.entity.Display; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.util.BoundingBox; +import org.bukkit.util.Transformation; +import org.bukkit.util.Vector; +import org.bukkit.util.VoxelShape; +import org.joml.AxisAngle4f; +import org.joml.Vector3f; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +public class BlockDisplayRaytracer implements Main { + + public static void cleanup() { + JavaPlugin plugin = main.getPlugin(); + List worlds = plugin.getServer().getWorlds(); + List entities = new ArrayList<>(); + for (World world : worlds) { + entities.addAll(world.getEntities().stream().filter(entity -> entity.getScoreboardTags().contains("$/TrimAlias/ Temp")).toList()); + entities.forEach(entity -> { + if (entity != null) entity.remove(); + }); + } + } + + public static void outline(Material display, Location location, long stayTime, List viewers) { + outline(display, location, 0.05, stayTime, viewers); + } + + public static void outline(Material display, Location corner1, Location corner2, double thickness, long stayTime, List viewers) { + World world = corner1.getWorld(); + + int minX = Math.min(corner1.getBlockX(), corner2.getBlockX()); + int minY = Math.min(corner1.getBlockY(), corner2.getBlockY()); + int minZ = Math.min(corner1.getBlockZ(), corner2.getBlockZ()); + int maxX = Math.max(corner1.getBlockX(), corner2.getBlockX()); + int maxY = Math.max(corner1.getBlockY(), corner2.getBlockY()); + int maxZ = Math.max(corner1.getBlockZ(), corner2.getBlockZ()); + + Location a1 = new Location(world, minX, minY, minZ); + Location a2 = new Location(world, maxX + 1, minY, minZ); + Location a3 = new Location(world, maxX + 1, minY, maxZ + 1); + Location a4 = new Location(world, minX, minY, maxZ + 1); + + Location b1 = new Location(world, minX, maxY + 1, minZ); + Location b2 = new Location(world, maxX + 1, maxY + 1, minZ); + Location b3 = new Location(world, maxX + 1, maxY + 1, maxZ + 1); + Location b4 = new Location(world, minX, maxY + 1, maxZ + 1); + + trace(display, a1, a2, thickness, stayTime, viewers); + trace(display, a2, a3, thickness, stayTime, viewers); + trace(display, a3, a4, thickness, stayTime, viewers); + trace(display, a4, a1, thickness, stayTime, viewers); + + trace(display, b1, b2, thickness, stayTime, viewers); + trace(display, b2, b3, thickness, stayTime, viewers); + trace(display, b3, b4, thickness, stayTime, viewers); + trace(display, b4, b1, thickness, stayTime, viewers); + + trace(display, a1, b1, thickness, stayTime, viewers); + trace(display, a2, b2, thickness, stayTime, viewers); + trace(display, a3, b3, thickness, stayTime, viewers); + trace(display, a4, b4, thickness, stayTime, viewers); + } + + + public static void outline(Material display, Location location, double thickness, long stayTime, List viewers) { + Location og = location.getBlock().getLocation(); + + Location a1 = og.clone().add(0, 0, 0); + Location a2 = og.clone().add(1, 0, 0); + Location a3 = og.clone().add(1, 0, 1); + Location a4 = og.clone().add(0, 0, 1); + + Location b1 = og.clone().add(0, 1, 0); + Location b2 = og.clone().add(1, 1, 0); + Location b3 = og.clone().add(1, 1, 1); + Location b4 = og.clone().add(0, 1, 1); + + trace(display, a1, a2, thickness, stayTime, viewers); + trace(display, a2, a3, thickness, stayTime, viewers); + trace(display, a3, a4, thickness, stayTime, viewers); + trace(display, a4, a1, thickness, stayTime, viewers); + + trace(display, b1, b2, thickness, stayTime, viewers); + trace(display, b2, b3, thickness, stayTime, viewers); + trace(display, b3, b4, thickness, stayTime, viewers); + trace(display, b4, b1, thickness, stayTime, viewers); + + trace(display, a1, b1, thickness, stayTime, viewers); + trace(display, a2, b2, thickness, stayTime, viewers); + trace(display, a3, b3, thickness, stayTime, viewers); + trace(display, a4, b4, thickness, stayTime, viewers); + } + + public static void trace(Material display, Location start, Location end, long stayTime, List viewers) { + trace(display, start, end.toVector().subtract(start.toVector()), 0.05, end.distance(start), stayTime, viewers); + } + + public static void trace(Material display, Location start, Location end, double thickness, long stayTime, List viewers) { + trace(display, start, end.toVector().subtract(start.toVector()), thickness, end.distance(start), stayTime, viewers); + } + + public static void trace(Material display, Location start, Vector direction, double thickness, double distance, long stayTime, List viewers) { + World world = start.getWorld(); + + BlockDisplay beam = world.spawn(start, BlockDisplay.class, entity -> { + AxisAngle4f angle = new AxisAngle4f(0, 0, 0, 1); + Vector3f transition = new Vector3f(-(float)(thickness / 2F)); + Vector3f scale = new Vector3f((float)thickness, (float)thickness, (float)distance); + Transformation trans = new Transformation(transition, angle, scale, angle); + Location vector = entity.getLocation(); + + vector.setDirection(direction); + entity.teleport(vector); + entity.setBlock(display.createBlockData()); + entity.setBrightness(new Display.Brightness(15, 15)); + entity.setInterpolationDelay(0); + entity.setTransformation(trans); + entity.addScoreboardTag("$/TrimAlias/ Temp"); + + for (Player player : Bukkit.getOnlinePlayers()) { + if (!viewers.contains(player)) { + player.hideEntity(main.getPlugin(), entity); + } + } + + Bukkit.getScheduler().runTaskLater(main.getPlugin(), entity::remove, stayTime); + }); + } + + public static void trace(Material display, Location start, Vector direction, double thickness, double distance, long stayTime, Consumer onEntitySpawn, List viewers) { + World world = start.getWorld(); + + BlockDisplay beam = world.spawn(start, BlockDisplay.class, entity -> { + AxisAngle4f angle = new AxisAngle4f(0, 0, 0, 1); + Vector3f transition = new Vector3f(-(float)(thickness / 2F)); + Vector3f scale = new Vector3f((float)thickness, (float)thickness, (float)distance); + Transformation trans = new Transformation(transition, angle, scale, angle); + Location vector = entity.getLocation(); + + vector.setDirection(direction); + entity.teleport(vector); + entity.setBlock(display.createBlockData()); + entity.setBrightness(new Display.Brightness(15, 15)); + entity.setInterpolationDelay(0); + entity.setTransformation(trans); + entity.addScoreboardTag("$/TrimAlias/ Temp"); + + + for (Player player : Bukkit.getOnlinePlayers()) { + if (!viewers.contains(player)) { + player.hideEntity(main.getPlugin(), entity); + } + } + + Bukkit.getScheduler().runTaskLater(main.getPlugin(), entity::remove, stayTime); + Bukkit.getScheduler().runTaskLater(main.getPlugin(), () -> onEntitySpawn.accept(entity), 5); + }); + } + + public static List outline(Material display, Location location, long stayTime) { + return outline(display, location, 0.05, stayTime); + } + + public static List outline(Material display, Location location, double thickness, long stayTime) { + Location og = location.getBlock().getLocation(); + + Location a1 = og.clone().add(0, 0, 0); + Location a2 = og.clone().add(1, 0, 0); + Location a3 = og.clone().add(1, 0, 1); + Location a4 = og.clone().add(0, 0, 1); + + Location b1 = og.clone().add(0, 1, 0); + Location b2 = og.clone().add(1, 1, 0); + Location b3 = og.clone().add(1, 1, 1); + Location b4 = og.clone().add(0, 1, 1); + + List a = new ArrayList<>(); + + a.add(trace(display, a1, a2, thickness, stayTime)); + a.add(trace(display, a2, a3, thickness, stayTime)); + a.add(trace(display, a3, a4, thickness, stayTime)); + a.add(trace(display, a4, a1, thickness, stayTime)); + + a.add(trace(display, b1, b2, thickness, stayTime)); + a.add(trace(display, b2, b3, thickness, stayTime)); + a.add(trace(display, b3, b4, thickness, stayTime)); + a.add(trace(display, b4, b1, thickness, stayTime)); + + a.add(trace(display, a1, b1, thickness, stayTime)); + a.add(trace(display, a2, b2, thickness, stayTime)); + a.add(trace(display, a3, b3, thickness, stayTime)); + a.add(trace(display, a4, b4, thickness, stayTime)); + + return a; + } + + public static void highlightCollisions(Block block, Color color, long stayTime) { + if (block == null || block.isEmpty() || !block.isCollidable()) + return; + + VoxelShape shape = block.getCollisionShape(); + World world = block.getWorld(); + Vector offset = block.getLocation().toVector(); + + for (BoundingBox box : shape.getBoundingBoxes()) { + highlight(box, offset, world, color, stayTime); + } + } + + public static void highlight(BoundingBox box, Vector offset, World world, Color color, long stayTime) { + double x1 = box.getMinX() + offset.getX(); + double y1 = box.getMinY() + offset.getY(); + double z1 = box.getMinZ() + offset.getZ(); + double x2 = box.getMaxX() + offset.getX(); + double y2 = box.getMaxY() + offset.getY(); + double z2 = box.getMaxZ() + offset.getZ(); + + traceGlowing(world, x1, y1, z1, x2, y1, z1, color, stayTime); + traceGlowing(world, x2, y1, z1, x2, y1, z2, color, stayTime); + traceGlowing(world, x2, y1, z2, x1, y1, z2, color, stayTime); + traceGlowing(world, x1, y1, z2, x1, y1, z1, color, stayTime); + + traceGlowing(world, x1, y2, z1, x2, y2, z1, color, stayTime); + traceGlowing(world, x2, y2, z1, x2, y2, z2, color, stayTime); + traceGlowing(world, x2, y2, z2, x1, y2, z2, color, stayTime); + traceGlowing(world, x1, y2, z2, x1, y2, z1, color, stayTime); + + traceGlowing(world, x1, y1, z1, x1, y2, z1, color, stayTime); + traceGlowing(world, x2, y1, z1, x2, y2, z1, color, stayTime); + traceGlowing(world, x2, y1, z2, x2, y2, z2, color, stayTime); + traceGlowing(world, x1, y1, z2, x1, y2, z2, color, stayTime); + } + + public static void traceGlowing(World world, double x1, double y1, double z1, double x2, double y2, double z2, Color color, long stayTime) { + Location loc1 = new Location(world, x1, y1, z1); + Location loc2 = new Location(world, x2, y2, z2); + BlockDisplay ent = trace(Material.WHITE_CONCRETE, loc1, loc2, 0.01, stayTime); + ent.setGlowColorOverride(color); + ent.setGlowing(true); + } + + public static BlockDisplay trace(Material display, Location start, Location end, long stayTime) { + return trace(display, start, end.toVector().subtract(start.toVector()), 0.05, end.distance(start), stayTime); + } + + public static BlockDisplay trace(Material display, Location start, Location end, double thickness, long stayTime) { + return trace(display, start, end.toVector().subtract(start.toVector()), thickness, end.distance(start), stayTime); + } + + public static BlockDisplay trace(Material display, Location start, Vector direction, double thickness, double distance, long stayTime) { + World world = start.getWorld(); + + BlockDisplay entity = world.spawn(start, BlockDisplay.class); + AxisAngle4f angle = new AxisAngle4f(0, 0, 0, 1); + Vector3f transition = new Vector3f(-(float)(thickness / 2F)); + Vector3f scale = new Vector3f((float)thickness, (float)thickness, (float)distance); + Transformation trans = new Transformation(transition, angle, scale, angle); + Location vector = entity.getLocation(); + + vector.setDirection(direction); + entity.teleport(vector); + entity.setBlock(display.createBlockData()); + entity.setBrightness(new Display.Brightness(15, 15)); + entity.setInterpolationDelay(0); + entity.setTransformation(trans); + entity.addScoreboardTag("$/TrimAlias/ Temp"); + + Bukkit.getScheduler().runTaskLater(main.getPlugin(), entity::remove, stayTime); + return entity; + } + + public static void transform(BlockDisplay display, Location start, Location end, double thickness) { + Vector direction = end.toVector().subtract(start.toVector()); + double distance = direction.length(); + + Location loc = start.clone(); + loc.setDirection(direction); + display.teleport(loc); + + Vector3f translation = new Vector3f(-(float)(thickness / 2F), 0, 0); // Centered + Vector3f scale = new Vector3f((float)thickness, (float)thickness, (float)distance); + AxisAngle4f rotation = new AxisAngle4f(0, 0, 0, 1); + Transformation transformation = new Transformation(translation, rotation, scale, rotation); + + display.setTransformation(transformation); + } + + public static void transform(BlockDisplay display, Location start, Vector direction, double distance, double thickness) { + Location loc = start.clone(); + loc.setDirection(direction); + display.teleport(loc); + + Vector3f translation = new Vector3f(-(float)(thickness / 2F), 0, 0); + Vector3f scale = new Vector3f((float)thickness, (float)thickness, (float)distance); + AxisAngle4f rotation = new AxisAngle4f(0, 0, 0, 1); + Transformation transformation = new Transformation(translation, rotation, scale, rotation); + + display.setTransformation(transformation); + } + + + public static void translate(BlockDisplay display, Vector3f offset) { + Transformation current = display.getTransformation(); + Vector3f translation = new Vector3f(current.getTranslation()).add(offset); + display.setTransformation(new Transformation( + translation, + current.getLeftRotation(), + current.getScale(), + current.getRightRotation() + )); + } + + public static void scale(BlockDisplay display, Vector3f scale) { + Transformation current = display.getTransformation(); + display.setTransformation(new Transformation( + current.getTranslation(), + current.getLeftRotation(), + scale, + current.getRightRotation() + )); + } + + public static void rotate(BlockDisplay display, AxisAngle4f rotation) { + Transformation current = display.getTransformation(); + display.setTransformation(new Transformation( + current.getTranslation(), + rotation, + current.getScale(), + rotation + )); + } + + public static void alignToDirection(BlockDisplay display, Vector direction) { + Location loc = display.getLocation().clone(); + loc.setDirection(direction); + display.teleport(loc); + } + +} diff --git a/src/main/java/me/trouper/alias/utils/visual/CustomDisplayRaytracer.java b/src/main/java/me/trouper/alias/utils/visual/CustomDisplayRaytracer.java new file mode 100755 index 0000000..000b53d --- /dev/null +++ b/src/main/java/me/trouper/alias/utils/visual/CustomDisplayRaytracer.java @@ -0,0 +1,317 @@ +package me.trouper.alias.utils.visual; + +import me.trouper.alias.utils.Verbose; +import org.bukkit.FluidCollisionMode; +import org.bukkit.Location; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.entity.Entity; +import org.bukkit.entity.LivingEntity; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.scheduler.BukkitTask; +import org.bukkit.util.BoundingBox; +import org.bukkit.util.RayTraceResult; +import org.bukkit.util.Vector; +import org.bukkit.util.VoxelShape; + +import java.util.List; +import java.util.Random; +import java.util.function.BiPredicate; +import java.util.function.Predicate; + +public class CustomDisplayRaytracer { + + public static final Predicate HIT_BLOCK = point -> { + Block b = point.getBlock(); + Location l = point.getLoc(); + + if (b == null || b.isEmpty() || !b.isCollidable()) + return false; + + Vector vec = l.toVector().subtract(b.getLocation().toVector()); + VoxelShape shape = b.getCollisionShape(); + + for (BoundingBox box : shape.getBoundingBoxes()) + if (box.contains(vec)) + return true; + return false; + }; + + public static final Predicate HIT_ENTITY = point -> { + return !point.getNearbyEntities(null, 5, true, 0.1, e -> e instanceof LivingEntity le && !le.isDead()).isEmpty(); + }; + + public static final Predicate HIT_BLOCK_OR_ENTITY = point -> { + return HIT_BLOCK.test(point) || HIT_ENTITY.test(point); + }; + + public static final Predicate HIT_BLOCK_AND_ENTITY = point -> { + return HIT_BLOCK.test(point) && HIT_ENTITY.test(point); + }; + + public static Predicate hitEntityExclude(Entity exclude) { + return point -> !point.getNearbyEntities(exclude, 5, true, 0.1, e -> e instanceof LivingEntity le && !le.isDead()).isEmpty(); + } + + public static Predicate hitAnythingExclude(Entity exclude) { + return point -> HIT_BLOCK.test(point) || !point.getNearbyEntities(exclude, 5, true, 0.1, e -> e instanceof LivingEntity le && !le.isDead()).isEmpty(); + } + + public static Predicate hitEverythingExclude(Entity exclude) { + return point -> HIT_BLOCK.test(point) && !point.getNearbyEntities(exclude, 5, true, 0.1, e -> e instanceof LivingEntity le && !le.isDead()).isEmpty(); + } + + public static Predicate hitEntityIf(Predicate condition) { + return point -> !point.getNearbyEntities(null, 5, true, 0.1, e -> e instanceof LivingEntity le && !le.isDead() && condition.test(e)).isEmpty(); + } + + public static Predicate hitBlockIf(Predicate condition) { + return point -> HIT_BLOCK.test(point) && condition.test(point.getBlock()); + } + + public static Predicate hitAnythingIf(Predicate condition) { + return point -> HIT_BLOCK.test(point) || !point.getNearbyEntities(null, 5, true, 0.1, e -> e instanceof LivingEntity le && !le.isDead() && condition.test(e)).isEmpty(); + } + + public static Predicate hitEverythingIf(Predicate condition) { + return point -> HIT_BLOCK.test(point) && !point.getNearbyEntities(null, 5, true, 0.1, e -> e instanceof LivingEntity le && !le.isDead() && condition.test(e)).isEmpty(); + } + + + public static Point trace(Location start, Location end, Predicate hitCondition) { + return trace(start, end, 0.5, hitCondition); + } + + public static Point trace(Location start, Location end, double interval, Predicate hitCondition) { + return trace(start, end.toVector().subtract(start.toVector()), end.distance(start), interval, hitCondition); + } + + public static Point trace(Location start, Vector direction, double distance, Predicate hitCondition) { + return trace(start, direction, distance, 0.5, hitCondition); + } + + public static Point trace(Location start, Vector direction, double distance, double interval, Predicate hitCondition) { + if (interval < 0) throw new IllegalArgumentException("interval cannot be zero!"); + if (distance < 0) throw new IllegalArgumentException("distance cannot be zero!"); + + for (double i = 0.0; i < distance; i += interval) { + Point point = blocksInFrontOf(start, direction, i, false); + if (hitCondition.test(point)) { + return point; + } + } + return blocksInFrontOf(start, direction, distance, true); + } + + + public static BukkitTask traceDelayed(Plugin plugin, Location start, Vector direction, double distance, double interval, long tickDelay, int pointsPerTick, Predicate hitCondition) { + + if (interval <= 0) throw new IllegalArgumentException("interval cannot be zero or negative!"); + if (distance <= 0) throw new IllegalArgumentException("distance cannot be zero or negative!"); + if (tickDelay < 0) throw new IllegalArgumentException("tickDelay cannot be negative!"); + + Vector normalizedDir = direction.clone().normalize(); + + + return new BukkitRunnable() { + private double currentDistance = 0.0; + private boolean hit = false; + + @Override + public void run() { + if (hit || currentDistance > distance) { + if (!hit) { + Point finalPoint = blocksInFrontOf(start, normalizedDir, distance, true); + hitCondition.test(finalPoint); + } + this.cancel(); + return; + } + + for (int i = 0; i < pointsPerTick && currentDistance <= distance; i++) { + Point point = blocksInFrontOf(start, normalizedDir, currentDistance, false); + if (hitCondition.test(point)) { + hit = true; + break; + } + currentDistance += interval; + } + } + }.runTaskTimer(plugin, 0, tickDelay); + } + + public static BukkitTask traceDelayed(Plugin plugin, Location start, Location end, double interval, long tickDelay, int pointsPerTick, Predicate hitCondition) { + Vector direction = end.toVector().subtract(start.toVector()).normalize(); + double distance = start.distance(end); + return traceDelayed(plugin, start, direction, distance, interval, tickDelay,pointsPerTick, hitCondition); + } + + public static BukkitTask traceDelayed(Plugin plugin, + Location start, + Location end, + long tickDelay, + Predicate hitCondition) { + return traceDelayed(plugin, start, end,0.5, tickDelay, 1, hitCondition); + } + + public static BukkitTask traceDelayed(Plugin plugin, + Location start, + Vector direction, + double distance, + long tickDelay, + Predicate hitCondition) { + return traceDelayed(plugin, start, direction, distance, 0.5, tickDelay,1, hitCondition); + } + + public static Point traceWithReflection(Location start, Vector direction, double distance, double interval, + int maxReflections, Predicate hitCondition, + BiPredicate blockReflectCondition, + BiPredicate entityReflectCondition) { + + + if (interval <= 0) throw new IllegalArgumentException("interval cannot be zero or negative!"); + if (distance <= 0) throw new IllegalArgumentException("distance cannot be zero or negative!"); + + Vector normalizedDir = direction.clone().normalize(); + Location currentLocation = start.clone(); + Vector currentDirection = normalizedDir.clone(); + double remainingDistance = distance; + int reflections = 0; + + while (remainingDistance > 0 && reflections <= maxReflections) { + for (double i = 0.0; i < remainingDistance; i += interval) { + Point point = blocksInFrontOf(currentLocation, currentDirection, i, false); + + if (hitCondition.test(point)) { + return point; + } + + boolean shouldReflect = false; + Vector newDirection = null; + + if (HIT_BLOCK.test(point) && point.getBlock() != null) { + Block hitBlock = point.getBlock(); + if (blockReflectCondition.test(point, hitBlock)) { + Point previousPoint = blocksInFrontOf(currentLocation, currentDirection, Math.max(0, i - interval), false); + + BlockFace hitFace = traceBlockFace(previousPoint.getLoc(), currentDirection, interval * 2); + + if (hitFace != null) { + Vector faceNormal = getFaceNormal(hitFace); + newDirection = calculateReflection(currentDirection, faceNormal); + shouldReflect = true; + } + } + } + + List nearbyEntities = point.getNearbyEntities(null, 5, true, 0.1, e -> e instanceof LivingEntity le && !le.isDead()); + if (!nearbyEntities.isEmpty()) { + for (Entity entity : nearbyEntities) { + if (entityReflectCondition.test(point, entity)) { + Point previousPoint = blocksInFrontOf(currentLocation, currentDirection, Math.max(0, i - interval), false); + + newDirection = glanceReflect(currentDirection); + shouldReflect = true; + break; + } + } + } + + if (shouldReflect) { + double backStep = Math.max(0, i - interval); + currentLocation = blocksInFrontOf(currentLocation, currentDirection, backStep, false).getLoc(); + + currentDirection = newDirection; + + remainingDistance -= backStep; + + reflections++; + + currentLocation = currentLocation.add(currentDirection.clone().multiply(interval * 0.1)); + remainingDistance -= interval * 0.1; + + break; + } + + if (i + interval >= remainingDistance) { + Point finalPoint = blocksInFrontOf(currentLocation, currentDirection, remainingDistance, true); + return finalPoint; + } + } + + if (reflections > maxReflections) { + Point finalPoint = blocksInFrontOf(currentLocation, currentDirection, remainingDistance, true); + return finalPoint; + } + } + + return blocksInFrontOf(start, normalizedDir, distance, true); + } + + private static Vector glanceReflect(Vector incident) { + return offsetVector(incident,4).multiply(-1); + } + + private static BlockFace traceBlockFace(Location startLocation, Vector direction, double maxDistance) { + Predicate blockPredicate = block -> true; + Predicate entityPredicate = entity -> false; + + RayTraceResult result = startLocation.getWorld().rayTrace(startLocation, direction, maxDistance, FluidCollisionMode.NEVER,true,0.1,entityPredicate, blockPredicate); + + if (result != null && result.getHitBlock() != null && result.getHitBlockFace() != null) { + return result.getHitBlockFace(); + } + + return null; + } + + private static Vector calculateReflection(Vector incident, Vector normal) { + // r = i - 2(i dot n)n + double dot = incident.dot(normal); + Vector reflection = incident.clone().subtract(normal.clone().multiply(2 * dot)); + + return reflection.normalize(); + } + + private static Vector getFaceNormal(BlockFace face) { + Verbose.send("Getting normal for %s".formatted(face)); + return switch (face) { + case DOWN -> new Vector(0, -1, 0); + case NORTH -> new Vector(0, 0, -1); + case SOUTH -> new Vector(0, 0, 1); + case EAST -> new Vector(1, 0, 0); + case WEST -> new Vector(-1, 0, 0); + default -> new Vector(0, 1, 0); + }; + } + + public static Point blocksInFrontOf(Location loc, Vector dir, double blocks, boolean missed) { + return new Point(loc.clone().add(dir.getX() * blocks, dir.getY() * blocks, dir.getZ() * blocks), blocks, missed); + } + + public static Vector offsetVector(Vector original, double angleDegrees) { + Random random = new Random(); + original = original.clone().normalize(); + + double yaw = Math.toDegrees(Math.atan2(-original.getX(), original.getZ())); + double pitch = Math.toDegrees(Math.asin(-original.getY())); + + double yawOffset = (random.nextDouble() * 2 - 1) * angleDegrees; + double pitchOffset = (random.nextDouble() * 2 - 1) * angleDegrees; + + yaw += yawOffset; + pitch += pitchOffset; + + pitch = Math.max(-90, Math.min(90, pitch)); + + double pitchRad = Math.toRadians(pitch); + double yawRad = Math.toRadians(yaw); + + double x = -Math.sin(yawRad) * Math.cos(pitchRad); + double y = -Math.sin(pitchRad); + double z = Math.cos(yawRad) * Math.cos(pitchRad); + + return new Vector(x, y, z); + } +} diff --git a/src/main/java/me/trouper/alias/utils/visual/DisplayUtils.java b/src/main/java/me/trouper/alias/utils/visual/DisplayUtils.java new file mode 100755 index 0000000..a0dfdd9 --- /dev/null +++ b/src/main/java/me/trouper/alias/utils/visual/DisplayUtils.java @@ -0,0 +1,247 @@ +package me.trouper.alias.utils.visual; + +import me.trouper.alias.server.Main; +import me.trouper.alias.utils.misc.Randomizer; +import org.bukkit.Bukkit; +import org.bukkit.Color; +import org.bukkit.Location; +import org.bukkit.Particle; +import org.bukkit.util.Vector; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; + +public class DisplayUtils implements Main { + + + public static void sphere(Location center, double radius, double pointDistance, Consumer action) { + double dPhi = pointDistance / radius; + + for (double phi = 0.0; phi <= Math.PI; phi += dPhi) { + double yOffset = radius * Math.cos(phi); + double ringRadius = radius * Math.sin(phi); + + if (ringRadius < 1e-6) { + Location loc = center.clone().add(0, yOffset, 0); + action.accept(loc); + } else { + double dTheta = pointDistance / ringRadius; + + for (double theta = 0.0; theta < 2 * Math.PI; theta += dTheta) { + double xOffset = ringRadius * Math.cos(theta); + double zOffset = ringRadius * Math.sin(theta); + + Location loc = center.clone().add(xOffset, yOffset, zOffset); + action.accept(loc); + } + } + } + } + + public static void sphereWave(Location center, double maxRadius, double radialStep, double maxDistanceBetweenPoints, Consumer action) { + AtomicReference currentRadius = new AtomicReference<>(radialStep); + + Bukkit.getScheduler().scheduleSyncRepeatingTask(main.getPlugin(), () -> { + double r = currentRadius.get(); + if (r > maxRadius) return; + + sphere(center, r, maxDistanceBetweenPoints, action); + currentRadius.set(r + radialStep); + }, 0L, 1L); + } + + + + public static final Function> PARTICLE_FACTORY = particle -> l -> l.getWorld().spawnParticle(particle, l, 1, 0, 0, 0, 0); + + public static final BiFunction> DUST_PARTICLE_FACTORY = (color, thickness) -> { + Particle.DustOptions dust = new Particle.DustOptions(color, thickness); + return l -> l.getWorld().spawnParticle(Particle.DUST, l, 1, 0, 0, 0, 0, dust); + }; + + public static final Function> FLAME_PARTICLE_FACTORY = soul -> { + Particle flame = soul ? Particle.SOUL_FIRE_FLAME : Particle.FLAME; + return l -> l.getWorld().spawnParticle(flame, l, 1, 0, 0, 0, 0); + }; + + public static void ring(Location loc, double radius, Color color, float thickness) { + ring(loc, radius, DUST_PARTICLE_FACTORY.apply(color, thickness)); + } + + public static void ring(Location loc, double radius, Consumer action) { + for (int theta = 0; theta < 360; theta += 10) { + double x = Math.cos(Math.toRadians(theta)) * radius; + double z = Math.sin(Math.toRadians(theta)) * radius; + Location newLoc = loc.clone().add(x, 0, z); + action.accept(newLoc); + } + } + + public static void ring(Location loc, double radius, double maxDistanceBetweenPoints, Consumer action) { + arc(loc, radius, 0, 360, maxDistanceBetweenPoints, action); + } + + public static void wave(Location loc, double radius, Color color, float thickness, double gap) { + wave(loc, radius, DUST_PARTICLE_FACTORY.apply(color, thickness), gap); + } + + public static void wave(Location loc, double radius, Consumer action, double gap) { + AtomicReference i = new AtomicReference<>(gap); + Bukkit.getScheduler().scheduleSyncRepeatingTask(main.getPlugin(), () -> { + if (i.get() >= radius) { + return; + } + ring(loc, i.get(), action); + i.set(i.get() + gap); + }, 0, 1); + } + + public static void wave(Location loc, double radius, double radialGap, double maxDistanceBetweenPoints, Consumer action) { + AtomicReference r = new AtomicReference<>(radialGap); + Bukkit.getScheduler().scheduleSyncRepeatingTask(main.getPlugin(), () -> { + if (r.get() > radius) return; + ring(loc, r.get(), maxDistanceBetweenPoints, action); + r.set(r.get() + radialGap); + }, 0, 1); + } + + public static void disc(Location loc, double radius, Consumer action, double gap) { + for (double i = gap; i < radius; i += gap) { + ring(loc, i, action); + } + } + + public static void disc(Location loc, double radius, double radialGap, double maxDistanceBetweenPoints, Consumer action) { + for (double r = radialGap; r <= radius; r += radialGap) { + ring(loc, r, maxDistanceBetweenPoints, action); + } + } + + public static void helix(Location loc, double radius, Consumer action, double gap, int height) { + int theta = 0; + for (double y = 0; y <= height; y += gap) { + double x = Math.cos(Math.toRadians(theta)) * radius; + double z = Math.sin(Math.toRadians(theta)) * radius; + + Location newLoc = loc.clone().add(x, y, z); + action.accept(newLoc); + theta += 10; + } + } + + public static void vortex(Location loc, double radius, Consumer action, double gapH, double gapV, int height) { + double r = radius; + int theta = 0; + for (double y = 0; y <= height; y += gapV) { + double x = Math.cos(Math.toRadians(theta)) * r; + double z = Math.sin(Math.toRadians(theta)) * r; + + Location newLoc = loc.clone().add(x, y, z); + action.accept(newLoc); + r += gapH; + theta += 10; + } + } + + public static void beam(Location loc, Consumer action, double gap, int height) { + for (double y = 0; y <= height; y += gap) { + Location newLoc = loc.clone().add(0, y, 0); + action.accept(newLoc); + } + } + + public static void arc(Location loc, double radius, int angleFrom, int angleTo, Consumer action) { + for (int theta = angleFrom; theta < angleTo; theta += 10) { + double x = Math.cos(Math.toRadians(theta)) * radius; + double z = Math.sin(Math.toRadians(theta)) * radius; + Location newLoc = loc.clone().add(x, 0, z); + action.accept(newLoc); + } + } + + public static void arc(Location loc, double radius, int angleFrom, int angleTo, double maxDistanceBetweenPoints, Consumer action) { + int angleSpan = angleTo - angleFrom; + if (angleSpan <= 0) return; + + int points = Math.max(2, (int) ((2 * Math.PI * radius * (angleSpan / 360.0)) / maxDistanceBetweenPoints)); + double angleStep = (double) angleSpan / points; + + for (int i = 0; i <= points; i++) { + double theta = angleFrom + (i * angleStep); + double x = Math.cos(Math.toRadians(theta)) * radius; + double z = Math.sin(Math.toRadians(theta)) * radius; + Location point = loc.clone().add(x, 0, z); + action.accept(point); + } + } + + + public static void fan(Location loc, double radius, int angleFrom, int angleTo, Consumer action, double gap) { + for (double i = gap; i < radius; i += gap) { + arc(loc, i, angleFrom, angleTo, action); + } + } + + public static void fan(Location loc, double radius, int angleFrom, int angleTo, double maxDistanceBetweenPoints, Consumer action, double radialGap) { + for (double r = radialGap; r < radius; r += radialGap) { + arc(loc, r, angleFrom, angleTo, maxDistanceBetweenPoints, action); + } + } + + + public static void fanWave(Location loc, double radius, int sections, Consumer action, double gap) { + double arcLength = 360.0 / sections; + AtomicReference i = new AtomicReference<>(0.0); + Bukkit.getScheduler().scheduleSyncRepeatingTask(main.getPlugin(), () -> { + if (i.get() >= 360) { + return; + } + double start = i.get(); + fan(loc, radius, (int)start, (int)(start + arcLength), action, gap); + i.set(i.get() + arcLength); + }, 0, 5); + } + + public static void fanWaveRandom(Location loc, double radius, int sections, Consumer action, double gap) { + double arcLength = 360.0 / sections; + List ints = new ArrayList<>(); + for (double start = 0; start < 360; start += arcLength) { + ints.add(start); + } + + AtomicInteger i = new AtomicInteger(0); + Randomizer random = new Randomizer(); + Bukkit.getScheduler().scheduleSyncRepeatingTask(main.getPlugin(), () -> { + if (i.get() >= sections) { + return; + } + double start = random.getRandomElement(ints); + ints.remove(start); + fan(loc, radius, (int)start, (int)(start + arcLength), action, gap); + i.getAndIncrement(); + }, 0, 5); + } + + public static void waveFan(Location loc, double radius, int angleFrom, int angleTo, double maxDistanceBetweenPoints, Consumer action, double radialGap) { + AtomicReference r = new AtomicReference<>(radialGap); + Bukkit.getScheduler().scheduleSyncRepeatingTask(main.getPlugin(), () -> { + if (r.get() >= radius) return; + arc(loc, r.get(), angleFrom, angleTo, maxDistanceBetweenPoints, action); + r.set(r.get() + radialGap); + }, 0, 1); + } + + public static void waveFan(Location loc, double radius, Vector direction, int angle, double maxDistanceBetweenPoints, Consumer action, double radialGap) { + double baseAngle = Math.toDegrees(Math.atan2(direction.getZ(), direction.getX())); + int angleFrom = (int) (baseAngle - angle / 2.0); + int angleTo = (int) (baseAngle + angle / 2.0); + waveFan(loc, radius, angleFrom, angleTo, maxDistanceBetweenPoints, action, radialGap); + } + + +} \ No newline at end of file diff --git a/src/main/java/me/trouper/alias/utils/visual/Point.java b/src/main/java/me/trouper/alias/utils/visual/Point.java new file mode 100755 index 0000000..e63e34e --- /dev/null +++ b/src/main/java/me/trouper/alias/utils/visual/Point.java @@ -0,0 +1,88 @@ +package me.trouper.alias.utils.visual; + +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.entity.Entity; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +public class Point { + private final Location loc; + private final World world; + private final Block block; + private final boolean missed; + private final double traveledDist; + private List rayPath; + + public Point(Location loc, double traveledDist, boolean missed) { + this.loc = loc; + this.world = loc.getWorld(); + this.block = loc.getBlock(); + this.missed = missed; + this.traveledDist = traveledDist; + + if (world == null) { + throw new IllegalArgumentException("point world cannot be null!"); + } + } + + public List getNearbyEntities(Entity exclude, int range, boolean requireContact, double expansionX, double expansionY, double expansionZ, Predicate filter) { + return new ArrayList<>(world.getNearbyEntities(loc, range, range, range, e -> { + if (requireContact && !e.getBoundingBox().expand(expansionX, expansionY, expansionZ).contains(loc.toVector())) { + return false; + } + return filter.test(e) && e != exclude; + })); + } + + public List getNearbyEntities(Entity exclude, int range, boolean requireContact, double expansion, Predicate filter) { + return getNearbyEntities(exclude, range, requireContact, expansion, expansion, expansion, filter); + } + + public List getNearbyEntities(Entity exclude, int range, boolean requireContact, Predicate filter) { + return getNearbyEntities(exclude, range, requireContact, 0, filter); + } + + public List getNearbyEntities(Entity exclude, int range, Predicate filter) { + return getNearbyEntities(exclude, range, false, filter); + } + + public double getTraveledDist() { + return traveledDist; + } + + public boolean wasMissed() { + return missed; + } + + public Block getBlock() { + return block; + } + + public Location getLoc() { + return loc; + } + + public World getWorld() { + return world; + } + + public double distance(Location other) { + return other.distance(loc); + } + + public Point addRayPoint(Point point) { + rayPath.add(point); + return point; + } + + public List setRayPath(List points) { + rayPath = points; + return rayPath; + } + + public List getRayPath = new ArrayList<>(); +} diff --git a/src/main/java/me/trouper/alias/utils/visual/ReflectionResult.java b/src/main/java/me/trouper/alias/utils/visual/ReflectionResult.java new file mode 100755 index 0000000..2f4bf79 --- /dev/null +++ b/src/main/java/me/trouper/alias/utils/visual/ReflectionResult.java @@ -0,0 +1,27 @@ +package me.trouper.alias.utils.visual; + +import org.bukkit.util.Vector; + +public class ReflectionResult { + private final Point hitPoint; + private final boolean shouldReflect; + private final Vector reflectedDirection; + + public ReflectionResult(Point hitPoint, boolean shouldReflect, Vector reflectedDirection) { + this.hitPoint = hitPoint; + this.shouldReflect = shouldReflect; + this.reflectedDirection = reflectedDirection; + } + + public Point getHitPoint() { + return hitPoint; + } + + public boolean shouldReflect() { + return shouldReflect; + } + + public Vector getReflectedDirection() { + return reflectedDirection; + } +} diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..dcf42f6 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,8 @@ +name: Alias +main: me.trouper.alias.Alias +version: 1.0.0 +authors: [ obvWolf ] +api-version: 1.21 +commands: + example_command: + description: Hello world command \ No newline at end of file