almost there

This commit is contained in:
Paldiu 2023-01-30 00:37:07 -06:00
parent b82ede0b9e
commit 2e5e341839
18 changed files with 497 additions and 66 deletions

View File

@ -16,13 +16,17 @@ repositories {
name = 'sonatype'
url = 'https://oss.sonatype.org/content/groups/public/'
}
maven {
name = 'jitpack'
url = 'https://jitpack.io'
}
}
dependencies {
implementation 'org.projectlombok:lombok:1.18.20'
implementation 'org.postgresql:postgresql:42.2.20'
implementation 'org.apache.commons:commons-lang3:3.12.0'
implementation 'com.github.MilkBowl:VaultAPI:1.7'
implementation 'com.github.Milkbowl:VaultAPI:1.7.1'
implementation 'com.google.code.gson:gson:2.8.7'
implementation 'org.jetbrains:annotations:22.0.0'
shadow 'io.projectreactor:reactor-core:3.4.10'

View File

@ -15,7 +15,9 @@ import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@CommandInfo(name = "ban",
description = "Ban a player. Use -n as the second parameter for the default ban reason.",
@ -67,4 +69,22 @@ public class BanCMD extends AbstractCommandBase {
manager.save();
return Component.text("Successfully banned user " + target.getName() + " for " + reason + " until " + expiryString).color(NamedTextColor.YELLOW);
}
@Override
public List<String> tab(CommandSender sender, String[] args) {
List<String> completions = new ArrayList<>();
if (args.length == 1) {
return Utilities.playerCompletions(args[0]);
}
if (args.length == 2) {
return Utilities.stringCompletions(args[1], "duration", "1d", "1h", "5m");
}
if (args.length == 3) {
return Utilities.stringCompletions(args[2], "reason", "-n");
}
return completions;
}
}

View File

@ -0,0 +1,107 @@
package mc.unraveled.reforged.command;
import mc.unraveled.reforged.api.annotations.CommandInfo;
import mc.unraveled.reforged.command.base.AbstractCommandBase;
import mc.unraveled.reforged.economy.EconomyRequest;
import mc.unraveled.reforged.plugin.Traverse;
import mc.unraveled.reforged.util.Utilities;
import net.kyori.adventure.text.Component;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.List;
@CommandInfo(
name = "bank",
description = "Bank command",
usage = "/bank <info | (pay | request <player> <amount>)>"
)
public class BankCMD extends AbstractCommandBase {
public BankCMD(Traverse plugin) {
super(plugin, "bank", false);
}
@Override
public Component cmd(CommandSender sender, String[] args) {
if (args.length < 1 || args.length > 3) return Component.text("Usage: /bank <info | (pay | request <player> <amount>)>");
Player player = (Player) sender;
if (args.length == 3) {
String action = args[0];
Player target = Bukkit.getPlayer(args[1]);
if (target == null) return MessageDefaults.MSG_NOT_FOUND;
int amount = Integer.parseInt(args[2]);
switch(action) {
case "pay":
if (amount < 0) return MessageDefaults.MSG_INVALID_AMOUNT;
if (getPlugin().getEconomyManager().transfer(player, target, amount).transactionSuccess()) {
return Component.text("You have paid " + target.getName() + " " + amount + " coins.");
} else {
return Component.text("You do not have enough coins to pay " + target.getName() + " " + amount + " coins.");
}
case "request":
if (amount < 0) return MessageDefaults.MSG_INVALID_AMOUNT;
getPlugin().getEconomyManager().request(player, target, amount);
break;
default:
return Component.text("Usage: /bank <info | (pay | request <player> <amount>)>");
}
}
if (args.length == 1) {
String action = args[0];
switch(action) {
case "info":
return Component.text("You have " + getPlugin().getEconomyManager().balance(player) + " coins.");
case "accept":
if (!getPlugin().getEconomyManager().getRequests().containsKey(player)) return Component.text("You do not have any pending requests.");
getPlugin().getEconomyManager().getRequests().get(player).forEach(EconomyRequest::accept);
// THIS IS GOING TO CHANGE, BY DEFAULT THIS ACCEPTS ALL REQUESTS. GOING TO IMPLEMENT A WAY TO ACCEPT EACH REQUEST INDIVIDUALLY.
break;
case "deny":
if (!getPlugin().getEconomyManager().getRequests().containsKey(player)) return Component.text("You do not have any pending requests.");
getPlugin().getEconomyManager().getRequests().get(player).forEach(EconomyRequest::deny);
// THIS IS GOING TO CHANGE, BY DEFAULT THIS DENIES ALL REQUESTS. GOING TO IMPLEMENT A WAY TO DENY EACH REQUEST INDIVIDUALLY.
break;
default:
return Component.text("Usage: /bank <info | (pay | request <player> <amount>)>");
}
}
return Component.empty();
}
@Override
public List<String> tab(CommandSender sender, String[] args) {
List<String> completions = new ArrayList<>();
if (args.length == 1) {
return Utilities.playerCompletions(args[0]);
}
if (args.length == 2) {
return Utilities.stringCompletions(args[1], "reset", "add", "set", "take");
}
if (args.length == 3) {
completions.add("amount");
return completions;
}
return completions;
}
}

View File

@ -141,12 +141,13 @@ public abstract class AbstractCommandBase extends TPermission implements IComman
}
protected static class MessageDefaults {
public static Component MSG_NO_PERMS = Component.text("You do not have permission to use this command!").color(NamedTextColor.RED);
public static Component MSG_NOT_PLAYER = Component.text("This command can only be run through the console.").color(NamedTextColor.RED);
public static Component MSG_NOT_FOUND = Component.text("Player not found.").color(NamedTextColor.RED);
public static Component MSG_NOT_CONSOLE = Component.text("This command can only be run by a player.").color(NamedTextColor.RED);
public static Component MSG_NOT_ENOUGH_ARGS = Component.text("Not enough arguments.").color(NamedTextColor.RED);
public static Component MUTED = Component.text("You are muted!").color(NamedTextColor.RED);
public static Component BANNED = Component.text("You are banned!").color(NamedTextColor.RED);
public static final Component MSG_INVALID_AMOUNT = Component.text("Amount must be greater than 0.").color(NamedTextColor.RED);
public static final Component MSG_NO_PERMS = Component.text("You do not have permission to use this command!").color(NamedTextColor.RED);
public static final Component MSG_NOT_PLAYER = Component.text("This command can only be run through the console.").color(NamedTextColor.RED);
public static final Component MSG_NOT_FOUND = Component.text("Player not found.").color(NamedTextColor.RED);
public static final Component MSG_NOT_CONSOLE = Component.text("This command can only be run by a player.").color(NamedTextColor.RED);
public static final Component MSG_NOT_ENOUGH_ARGS = Component.text("Not enough arguments.").color(NamedTextColor.RED);
public static final Component MUTED = Component.text("You are muted!").color(NamedTextColor.RED);
public static final Component BANNED = Component.text("You are banned!").color(NamedTextColor.RED);
}
}

View File

@ -10,7 +10,7 @@ import org.jetbrains.annotations.NotNull;
import java.io.File;
@Getter
abstract class Yaml extends YamlConfiguration implements Baker {
public abstract class Yaml extends YamlConfiguration implements Baker {
private final String fileName;
private final File dataFolder;
private final Traverse plugin;

View File

@ -0,0 +1,11 @@
package mc.unraveled.reforged.data;
import lombok.Data;
import net.kyori.adventure.text.Component;
import org.bukkit.entity.Player;
@Data
public class CustomLoginData {
private final Player player;
private final Component loginMessage;
}

View File

@ -14,7 +14,7 @@ import java.util.Map;
public class InfractionData {
private static final Map<OfflinePlayer, InfractionData> playerInfractionDataMap = new HashMap<>() {{
DBInfraction infraction = new DBInfraction(JavaPlugin.getPlugin(Traverse.class).getSQLManager().establish());
for (InfractionData data : infraction.getStoredInfractionsFromUUID()) {
for (InfractionData data : infraction.getInfractionsList()) {
put(data.getPlayer(), data);
}
}};

View File

@ -0,0 +1,87 @@
package mc.unraveled.reforged.data;
import lombok.Getter;
import mc.unraveled.reforged.api.Baker;
import mc.unraveled.reforged.listening.AbstractListener;
import mc.unraveled.reforged.plugin.Traverse;
import mc.unraveled.reforged.storage.DBUser;
import mc.unraveled.reforged.util.Pair;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerLoginEvent;
import org.jetbrains.annotations.Nullable;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
@Getter
public class LoginManager implements Baker {
private final Traverse plugin;
private Set<Pair<Player, CustomLoginData>> dataSet = new HashSet<>(); // VALUE ONLY MODIFIED BY BAKER
private boolean baked = false;
public LoginManager(Traverse plugin) {
this.plugin = plugin;
new LoginListener(plugin);
}
public void add(Player player, CustomLoginData data) {
dataSet.add(new Pair<>(player, data));
}
public void remove(Player player) {
dataSet.removeIf(pair -> pair.getFirst().equals(player));
}
@Nullable
public CustomLoginData get(Player player) {
return dataSet.stream()
.filter(pair -> pair.getFirst().equals(player))
.map(Pair::getSecond)
.findFirst()
.orElse(null);
}
public void set(Player player, CustomLoginData data) {
remove(player);
add(player, data);
}
@Override
public void bake() {
if (baked) return;
dataSet.forEach(pair -> {
Player player = pair.getFirst();
CustomLoginData data = pair.getSecond();
DBUser user = new DBUser(plugin.getSQLManager().establish());
user.setLoginMessage(player.getUniqueId().toString(), data.getLoginMessage().toString());
user.close();
});
this.dataSet = dataSet.stream().collect(Collectors.toUnmodifiableSet());
this.baked = true;
}
@Override
public void unbake() {
if (!baked) return;
this.dataSet = new HashSet<>(dataSet);
this.baked = false;
}
private final class LoginListener extends AbstractListener {
public LoginListener(Traverse plugin) {
super(plugin);
}
@EventHandler
public void onLogin(PlayerLoginEvent event) {
Player player = event.getPlayer();
}
}
}

View File

@ -16,5 +16,6 @@ public class PlayerData {
private long playtime;
private int coins;
private Date lastLogin;
private String loginMessage;
private InfractionData infractionData;
}

View File

@ -1,33 +0,0 @@
package mc.unraveled.reforged.data;
import mc.unraveled.reforged.permission.Rank;
import mc.unraveled.reforged.plugin.Traverse;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import java.time.Instant;
import java.util.Date;
public class PlayerDataListener implements Listener {
private final Traverse plugin;
public PlayerDataListener(Traverse plugin) {
plugin.getServer().getPluginManager().registerEvents(this, plugin);
this.plugin = plugin;
}
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
PlayerData data = plugin.getDataManager().getPlayerData(player.getUniqueId());
if (data == null) {
data = new PlayerData(player.getUniqueId(), player.getName(), Rank.NON_OP, 0L, 0, Date.from(Instant.now()), InfractionData.getCachedInfractionData(player));
plugin.getDataManager().addPlayerData(data);
}
data.setLastLogin(Date.from(Instant.now()));
}
}

View File

@ -1,5 +1,87 @@
package mc.unraveled.reforged.economy;
import lombok.Getter;
import mc.unraveled.reforged.plugin.Traverse;
import net.milkbowl.vault.economy.Economy;
import net.milkbowl.vault.economy.EconomyResponse;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.bukkit.plugin.RegisteredServiceProvider;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.MissingResourceException;
@Getter
public class EconomyManager {
// TODO: Implement economy
private final RegisteredServiceProvider<Economy> rsp;
private final Map<Player, List<EconomyRequest>> requests = new HashMap<>(); // This is NOT persistent.
private final Economy economy;
private final Coin coin;
public EconomyManager(@NotNull Traverse plugin) {
if (plugin.getServer().getPluginManager().getPlugin("Vault") == null)
throw new RuntimeException("Vault is REQUIRED!");
rsp = plugin.getServer().getServicesManager().getRegistration(Economy.class);
if (rsp == null) throw new MissingResourceException("Economy not found", Economy.class.getName(), "Economy");
economy = rsp.getProvider();
coin = new Coin("Sheccies", "$", 1);
}
public EconomyResponse newAccount(OfflinePlayer player) {
return economy.createBank(player.getName(), player);
}
public EconomyResponse balance(@NotNull OfflinePlayer player) {
return economy.bankBalance(player.getName());
}
public EconomyRequest request(@NotNull OfflinePlayer from, @NotNull OfflinePlayer to, int amount) {
EconomyRequest request = new EconomyRequest(this, (Player) from, (Player) to, amount);
if (requests.containsKey(to)) {
requests.get(to).add(request);
} else {
requests.put((Player) to, List.of(request));
}
return request;
}
public EconomyResponse deposit(@NotNull OfflinePlayer player, double amount) {
return economy.bankDeposit(player.getName(), amount);
}
public EconomyResponse withdraw(@NotNull OfflinePlayer player, double amount) {
return economy.bankWithdraw(player.getName(), amount);
}
public EconomyResponse transfer(OfflinePlayer from, OfflinePlayer to, double amount) {
EconomyResponse response = withdraw(from, amount);
if (response.transactionSuccess()) {
response = deposit(to, amount);
if (!response.transactionSuccess()) {
deposit(from, amount);
}
}
return response;
}
public EconomyResponse delete(@NotNull OfflinePlayer player) {
return economy.deleteBank(player.getName());
}
public EconomyResponse has(@NotNull OfflinePlayer player, double amount) {
return economy.bankHas(player.getName(), amount);
}
public boolean hasAccount(@NotNull OfflinePlayer player) {
return economy.hasBankSupport() && economy.hasAccount(player);
}
}

View File

@ -0,0 +1,48 @@
package mc.unraveled.reforged.economy;
import lombok.Getter;
import lombok.Setter;
import net.kyori.adventure.text.Component;
import net.milkbowl.vault.economy.Economy;
import net.milkbowl.vault.economy.EconomyResponse;
import org.bukkit.entity.Player;
public class EconomyRequest {
@Getter
private final Player sender;
@Getter
private final Player target;
@Getter
private final int amount;
private final EconomyManager economy;
public EconomyRequest(EconomyManager economy, Player sender, Player target, int amount) {
this.sender = sender;
this.target = target;
this.amount = amount;
this.economy = economy;
}
public EconomyResponse accept() {
EconomyResponse r = economy.withdraw(target, amount);
if (r.transactionSuccess()) {
r = economy.deposit(sender, amount);
if (!r.transactionSuccess()) {
economy.deposit(target, amount);
}
}
sender.sendMessage(Component.text("Your request has been accepted."));
target.sendMessage(Component.text("You have accepted " + sender.getName() + "'s request."));
economy.getRequests().get(target).remove(this);
return r;
}
public void deny() {
sender.sendMessage(Component.text("Your request has been denied."));
target.sendMessage(Component.text("You have denied " + sender.getName() + "'s request."));
economy.getRequests().get(target).remove(this);
}
}

View File

@ -1,11 +1,9 @@
package mc.unraveled.reforged.listening;
import lombok.AllArgsConstructor;
import lombok.Data;
import mc.unraveled.reforged.plugin.Traverse;
import org.bukkit.event.Listener;
@AllArgsConstructor
@Data
public class AbstractListener implements Listener {
private final Traverse plugin;

View File

@ -0,0 +1,45 @@
package mc.unraveled.reforged.listening;
import mc.unraveled.reforged.data.InfractionData;
import mc.unraveled.reforged.data.PlayerData;
import mc.unraveled.reforged.permission.Rank;
import mc.unraveled.reforged.plugin.Traverse;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import java.time.Instant;
import java.util.Date;
public class PlayerDataListener extends AbstractListener {
public PlayerDataListener(Traverse plugin) {
super(plugin);
}
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
PlayerData data = getPlugin().getDataManager().getPlayerData(player.getUniqueId());
if (data == null) {
data = new PlayerData(player.getUniqueId(),
player.getName(),
Rank.NON_OP,
0L,
0,
Date.from(Instant.now()),
null,
InfractionData.getCachedInfractionData(player));
getPlugin().getDataManager().addPlayerData(data);
}
if (getPlugin().getEconomyManager().hasAccount(player)) {
data.setCoins((int) getPlugin().getEconomyManager().balance(player).balance);
} else {
getPlugin().getEconomyManager().newAccount(player);
}
data.setLastLogin(Date.from(Instant.now()));
}
}

View File

@ -4,11 +4,13 @@ import lombok.Getter;
import lombok.SneakyThrows;
import mc.unraveled.reforged.api.Locker;
import mc.unraveled.reforged.banning.BanManager;
import mc.unraveled.reforged.command.BanCMD;
import mc.unraveled.reforged.command.GroupCMD;
import mc.unraveled.reforged.command.TraverseCMD;
import mc.unraveled.reforged.command.*;
import mc.unraveled.reforged.command.base.CommandLoader;
import mc.unraveled.reforged.config.YamlManager;
import mc.unraveled.reforged.data.DataManager;
import mc.unraveled.reforged.data.LoginManager;
import mc.unraveled.reforged.economy.EconomyManager;
import mc.unraveled.reforged.listening.InfractionListener;
import mc.unraveled.reforged.permission.RankManager;
import mc.unraveled.reforged.service.base.Scheduling;
import mc.unraveled.reforged.service.base.ServicePool;
@ -33,6 +35,12 @@ public final class Traverse extends JavaPlugin implements Locker {
private Scheduling scheduler;
@Getter
private ServicePool PIPELINE;
@Getter
private EconomyManager economyManager;
@Getter
private LoginManager loginManager;
@Getter
private YamlManager yamlManager;
@Override
@SneakyThrows
@ -44,6 +52,12 @@ public final class Traverse extends JavaPlugin implements Locker {
this.rankManager = new RankManager(this);
this.scheduler = new Scheduling(this);
this.PIPELINE = new ServicePool("PIPELINE", this);
this.economyManager = new EconomyManager(this);
this.loginManager = new LoginManager(this);
this.yamlManager = new YamlManager(this);
registerCommands();
registerListeners();
}
@Override
@ -58,13 +72,24 @@ public final class Traverse extends JavaPlugin implements Locker {
@SneakyThrows
public void registerCommands() {
synchronized (lock()) {
getCommandLoader().register(new TraverseCMD(this),
getCommandLoader().register(
new BanCMD(this),
new GroupCMD(this));
new BankCMD(this),
new EntityPurgeCMD(this),
new GroupCMD(this),
new MuteCMD(this),
new PardonCMD(this),
new TraverseCMD(this),
new UnmuteCMD(this)
);
lock().wait(1000);
}
lock().notify();
getCommandLoader().load();
}
public void registerListeners() {
new InfractionListener(this);
}
}

View File

@ -44,11 +44,18 @@ public class DBInfraction {
SQLConst.END +
SQLConst.SEMICOLON);
statement.setString(1, player.getUniqueId().toString());
statement.setInt(2, data.getInfractions());
statement.setBoolean(3, data.isMuted());
statement.setBoolean(4, data.isFrozen());
statement.setBoolean(5, data.isLocked());
statement.setBoolean(6, data.isJailed());
statement.setString(2, player.getUniqueId().toString());
statement.setInt(3, data.getInfractions());
statement.setBoolean(4, data.isMuted());
statement.setBoolean(5, data.isFrozen());
statement.setBoolean(6, data.isLocked());
statement.setBoolean(7, data.isJailed());
statement.setInt(8, data.getInfractions());
statement.setBoolean(9, data.isMuted());
statement.setBoolean(10, data.isFrozen());
statement.setBoolean(11, data.isLocked());
statement.setBoolean(12, data.isJailed());
statement.setString(13, player.getUniqueId().toString());
statement.executeUpdate();
}
@ -61,7 +68,7 @@ public class DBInfraction {
}
@SneakyThrows
public List<InfractionData> getStoredInfractionsFromUUID() {
public List<InfractionData> getInfractionsList() {
List<InfractionData> temp = new ArrayList<>();
PreparedStatement statement = connection.prepareStatement(SELECT);

View File

@ -26,15 +26,15 @@ public class DBUser {
@SneakyThrows
public void createTable() {
PreparedStatement statement = getConnection().prepareStatement("CREATE TABLE IF NOT EXISTS users (uuid VARCHAR(36), username VARCHAR(16), rank VARCHAR(64), play_time BIGINT, coins BIGINT, last_login BIGINT, PRIMARY KEY (uuid));");
PreparedStatement statement = getConnection().prepareStatement("CREATE TABLE IF NOT EXISTS users (uuid VARCHAR(36), username VARCHAR(16), rank VARCHAR(64), play_time BIGINT, coins BIGINT, last_login BIGINT, login_message VARCHAR(64), PRIMARY KEY (uuid));");
statement.executeUpdate();
}
@SneakyThrows
public void insert(@NotNull PlayerData playerData) {
PreparedStatement statement = getConnection().prepareStatement("IF NOT EXISTS (SELECT 1 FROM users WHERE uuid = ?) " +
"BEGIN INSERT INTO users (uuid, username, rank, play_time, coins, last_login) VALUES (?, ?, ?, ?, ?, ?) END " +
"ELSE BEGIN UPDATE users SET username = ?, rank = ?, play_time = ?, coins = ?, last_login = ? WHERE uuid = ? END;");
"BEGIN INSERT INTO users (uuid, username, rank, play_time, coins, last_login, login_message) VALUES (?, ?, ?, ?, ?, ?, ?) END " +
"ELSE BEGIN UPDATE users SET username = ?, rank = ?, play_time = ?, coins = ?, last_login = ?, login_message = ?, WHERE uuid = ? END;");
statement.setString(1, playerData.getUuid().toString());
statement.setString(2, playerData.getUuid().toString());
@ -43,12 +43,14 @@ public class DBUser {
statement.setLong(5, playerData.getPlaytime());
statement.setInt(6, playerData.getCoins());
statement.setLong(7, playerData.getLastLogin().getTime());
statement.setString(8, playerData.getUserName());
statement.setString(9, playerData.getRank().name());
statement.setLong(10, playerData.getPlaytime());
statement.setInt(11, playerData.getCoins());
statement.setLong(12, playerData.getLastLogin().getTime());
statement.setString(13, playerData.getUuid().toString());
statement.setString(8, playerData.getLoginMessage());
statement.setString(9, playerData.getUserName());
statement.setString(10, playerData.getRank().name());
statement.setLong(11, playerData.getPlaytime());
statement.setInt(12, playerData.getCoins());
statement.setLong(13, playerData.getLastLogin().getTime());
statement.setString(14, playerData.getLoginMessage());
statement.setString(15, playerData.getUuid().toString());
statement.executeUpdate();
}
@ -66,6 +68,14 @@ public class DBUser {
statement.executeUpdate();
}
@SneakyThrows
public void setLoginMessage(String uuid, String message) {
PreparedStatement statement = getConnection().prepareStatement("UPDATE users SET login_message = ? WHERE uuid = ?;");
statement.setString(1, message);
statement.setString(2, uuid);
statement.executeUpdate();
}
@SneakyThrows
public List<PlayerData> all() {
PreparedStatement statement = getConnection().prepareStatement("SELECT * FROM users;");
@ -80,6 +90,7 @@ public class DBUser {
resultSet.getLong("play_time"),
resultSet.getInt("coins"),
new Date(resultSet.getLong("last_login")),
resultSet.getString("login_message"),
data);
dataList.add(playerData);
}

View File

@ -8,8 +8,10 @@ import reactor.core.publisher.Mono;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
public final class Utilities {
@ -115,4 +117,19 @@ public final class Utilities {
public static Mono<OfflinePlayer> getOfflinePlayer(String name) {
return Mono.create(sink -> sink.success(Bukkit.getOfflinePlayer(name)));
}
public static List<String> playerCompletions(String arg) {
return Bukkit.getOnlinePlayers()
.stream()
.map(OfflinePlayer::getName)
.filter(Objects::nonNull)
.filter(s -> s.startsWith(arg))
.toList();
}
public static List<String> stringCompletions(String arg, String... completions) {
return Stream.of(completions)
.filter(s -> s.startsWith(arg))
.toList();
}
}