diff --git a/src/main/java/me/re4erka/lpmetaplus/LPMetaPlus.java b/src/main/java/me/re4erka/lpmetaplus/LPMetaPlus.java new file mode 100644 index 0000000..15c28aa --- /dev/null +++ b/src/main/java/me/re4erka/lpmetaplus/LPMetaPlus.java @@ -0,0 +1,161 @@ +package me.re4erka.lpmetaplus; + +import com.google.common.collect.Lists; +import de.exlll.configlib.NameFormatters; +import de.exlll.configlib.YamlConfigurationProperties; +import de.exlll.configlib.YamlConfigurations; +import dev.triumphteam.cmd.bukkit.BukkitCommandManager; +import dev.triumphteam.cmd.bukkit.message.BukkitMessageKey; +import dev.triumphteam.cmd.core.message.MessageKey; +import dev.triumphteam.cmd.core.suggestion.SuggestionKey; +import lombok.Getter; +import lombok.experimental.Accessors; +import me.re4erka.lpmetaplus.command.type.CustomCommand; +import me.re4erka.lpmetaplus.command.type.MainCommand; +import me.re4erka.lpmetaplus.configuration.ConfigurationMetas; +import me.re4erka.lpmetaplus.manager.type.GroupManager; +import me.re4erka.lpmetaplus.manager.type.MetaManager; +import me.re4erka.lpmetaplus.message.Message; +import me.re4erka.lpmetaplus.placeholder.MetaPlaceholder; +import me.re4erka.lpmetaplus.plugin.BasePlugin; +import me.re4erka.lpmetaplus.util.PluginEmulator; +import net.luckperms.api.LuckPermsProvider; +import net.luckperms.api.model.PermissionHolder; +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.stream.Collectors; + +@Getter +public final class LPMetaPlus extends BasePlugin { + + @Accessors(fluent = true) + private Settings settings; + @Accessors(fluent = true) + private Messages messages; + + private GroupManager groupManager; + + @Accessors(fluent = true) + private ConfigurationMetas metas; + private MetaManager metaManager; + + @Override + public void enable() { + initialize("MetaManager", plugin -> this.metaManager = new MetaManager(plugin)); + initialize("GroupManager", plugin -> this.groupManager = new GroupManager(plugin)); + + initialize("Emulation", plugin -> { + if (settings.emulation().enabled()) { + settings.emulation().applyTo().forEach(emulation -> { + if (Bukkit.getPluginManager().isPluginEnabled(emulation.getPluginName())) { + logError("The " + emulation.getPluginName() + " plugin cannot be emulated as it is already running on the server. " + + "Please disable the " + emulation.getPluginName() + " plugin to make the emulation work correctly."); + return; + } + + if (settings.emulation().emulateLookupNames()) { + PluginEmulator.emulate(emulation.getPluginName()); + } + + emulation.load(plugin); + logInfo(emulation.getPluginName() + " plugin is successfully emulated."); + }); + } + }); + + initialize("MetaPlaceholder", plugin -> { + if (isSupportPlaceholderAPI()) { + new MetaPlaceholder(plugin).register(); + } else { + logNotFoundPlaceholderAPI(); + } + }); + + System.out.println("CoinsEngine isEnabled(): " + Bukkit.getPluginManager().isPluginEnabled("CoinsEngine")); + System.out.println("PlayerPoints isEnabled(): " + Bukkit.getPluginManager().isPluginEnabled("PlayerPoints")); + + Bukkit.getScheduler().runTaskLater(this, () -> { + settings.emulation().applyTo().forEach(emulation -> { + PluginEmulator.emulate(emulation.getPluginName()); + }); + + System.out.println("CoinsEngine isEnabled(): " + Bukkit.getPluginManager().isPluginEnabled("CoinsEngine")); + System.out.println("PlayerPoints isEnabled(): " + Bukkit.getPluginManager().isPluginEnabled("PlayerPoints")); + }, 3); + + groupManager.load(metas); + metaManager.registerWarningEvents(); + } + + @Override + protected void loadConfigurations() { + final YamlConfigurationProperties properties = YamlConfigurationProperties.newBuilder() + .charset(StandardCharsets.UTF_8) + .setNameFormatter(NameFormatters.LOWER_UNDERSCORE) + .addSerializer(Message.class, new Message.Serializer()) + .build(); + + final Path dataFolder = getDataFolder().toPath(); + this.settings = YamlConfigurations.update( + dataFolder.resolve("config.yml"), + Settings.class, properties + ); + this.messages = YamlConfigurations.update( + dataFolder.resolve("messages.yml"), + Messages.class, properties + ); + this.metas = YamlConfigurations.update( + dataFolder.resolve("metas.yml"), + ConfigurationMetas.class, properties + ); + } + + @Override + protected void registerCommands() { + final BukkitCommandManager manager = BukkitCommandManager.create(this); + + final Messages.Command messages = messages().command(); + manager.registerMessage(MessageKey.TOO_MANY_ARGUMENTS, + (sender, context) -> messages.tooManyArguments().send(sender)); + manager.registerMessage(MessageKey.NOT_ENOUGH_ARGUMENTS, + (sender, context) -> messages.notEnoughArguments().send(sender)); + manager.registerMessage(MessageKey.INVALID_ARGUMENT, + (sender, context) -> messages.invalidArguments().send(sender)); + manager.registerMessage(MessageKey.UNKNOWN_COMMAND, + (sender, context) -> messages.unknownCommand().send(sender)); + manager.registerMessage(BukkitMessageKey.NO_PERMISSION, + (sender, context) -> messages.noPermission().send(sender)); + + manager.registerSuggestion( + SuggestionKey.of("meta_types"), + (sender, argument) -> metas.names() + ); + manager.registerSuggestion( + SuggestionKey.of("any_count"), + (sender, argument) -> Lists.newArrayList("1", "10", "50", "100") + ); + manager.registerSuggestion( + SuggestionKey.of("loaded_user_names"), + (sender, argument) -> LuckPermsProvider.get().getUserManager() + .getLoadedUsers().stream() + .map(PermissionHolder::getFriendlyName) + .collect(Collectors.toList()) + ); + + manager.registerCommand(new MainCommand(this)); + metas.types().entrySet().stream() + .filter(entry -> entry.getValue().isCommandEnabled()) + .forEach(entry -> manager.registerCommand( + new CustomCommand(this, entry.getKey(), entry.getValue()))); + } + + @NotNull + @Override + protected LPMetaPlus self() { + return this; + } +} diff --git a/src/main/java/me/re4erka/lpmetaplus/Messages.java b/src/main/java/me/re4erka/lpmetaplus/Messages.java new file mode 100644 index 0000000..b1ccadb --- /dev/null +++ b/src/main/java/me/re4erka/lpmetaplus/Messages.java @@ -0,0 +1,65 @@ +package me.re4erka.lpmetaplus; + +import de.exlll.configlib.Configuration; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import me.re4erka.lpmetaplus.message.Message; + +import java.util.Arrays; +import java.util.List; + +@Getter +@Accessors(fluent = true) +@Configuration +@SuppressWarnings("FieldMayBeFinal") +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class Messages { + + private Meta meta = new Meta(); + + @Getter + @Accessors(fluent = true) + @Configuration + public static final class Meta { + + private Message get = Message.of("&fУ игрока &6%target% &fмета-данной &r%display_name% &fв количестве&8: &e%balance% %symbol%"); + private Message set = Message.of("&fИгроку &6%target% &fбыло установлено мета-данной &r%display_name%&8: &e%balance% %symbol%"); + private Message given = Message.of("&fИгроку &6%target% &fбыла выдана мета-данная &r%display_name%&8: &e%balance% %symbol%"); + private Message taken = Message.of("&fИгроку &6%target% &fбыло отнято мета-данных &r%display_name%&8: &e%balance% %symbol%"); + private Message reset = Message.of("&fИгроку &6%target% &fбыл сброшен баланс мета-данной &r%display_name%&f!"); + + private Message notFound = Message.of("&fВведенная вами мета-данная &cне была найдена&f!"); + private Message unsignedNotSupported = Message.of("&fБеззнаковые значения &cне поддерживаются&f!"); + } + + private Command command = new Command(); + + @Getter + @Accessors(fluent = true) + @Configuration + public static final class Command { + + private Message unknownCommand = Message.of("&fВведенная вами команда &cне была найдена&f!"); + private Message tooManyArguments = Message.of("&fВами введенно &cслишком много &fаргументов!"); + private Message notEnoughArguments = Message.of("&fВами введенно &cнедостаточно &fаргументов!"); + private Message invalidArguments = Message.of("&fВведенные вами аргументы &cнекорректны&f! Используйте &e/lpmetaplus help &fдля помощи"); + + private Message noPermission = Message.of("&fУ вас &cнедостаточно прав&f, чтобы использовать эту команду!"); + + private Message migrationInProgress = Message.of("&fМиграция из плагина &a%name% &fв процессе..."); + private Message migrated = Message.of("&fПлагин &aуспешно мигрировал &fигроков &e%count% &fза &6%took%ms &fиз плагина &a%name%&f."); + private Message migrationFailed = Message.of("&fМиграция из плагина &a%name% &fбыла &cпровалена&f! Заняло &6%took%ms"); + + private Message reloaded = Message.of("&fПлагин был &aуспешно перезагружен&f!"); + private List help = Arrays.asList( + Message.of("&fДоступные команды&8:"), + Message.of("&8- &e/lpmetaplus get &f<тип> <ник>"), + Message.of("&8- &e/lpmetaplus set &f<тип> <количество> <ник> &7(-silent)"), + Message.of("&8- &e/lpmetaplus take &f<тип> <количество> <ник> &7(-silent)"), + Message.of("&8- &e/lpmetaplus give &f<тип> <количество> <ник> &7(-silent)"), + Message.of("&8- &e/lpmetaplus reload") + ); + } +} diff --git a/src/main/java/me/re4erka/lpmetaplus/Settings.java b/src/main/java/me/re4erka/lpmetaplus/Settings.java new file mode 100644 index 0000000..2bbcc52 --- /dev/null +++ b/src/main/java/me/re4erka/lpmetaplus/Settings.java @@ -0,0 +1,88 @@ +package me.re4erka.lpmetaplus; + +import com.google.common.collect.Sets; +import de.exlll.configlib.Comment; +import de.exlll.configlib.Configuration; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import me.re4erka.lpmetaplus.emulation.SupportedEmulation; + +import java.util.Set; + +@Getter +@Accessors(fluent = true) +@Configuration +@SuppressWarnings("FieldMayBeFinal") +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class Settings { + + @Comment({"Миграция с других плагинов на донатную валюту.", + "Например: /lpmetaplus migrate PLAYER_POINTS SQLITE"}) + private Migration migration = new Migration(); + + @Getter + @Accessors(fluent = true) + @Configuration + public static final class Migration { + + @Comment({"Тип мета-данной по-умолчанию для миграции.", + "Какой тип донатной валюты будет мигрироваться из плагина PlayerPoints?", + "Требуется обязательно указать существующий тип плагина LPMetaPlus."}) + private String defaultType = "RUBIES"; + + @Comment("Настройка подключения к базе-данных для миграции.") + private Credentials credentials = new Credentials(); + + @Getter + @Accessors(fluent = true) + @Configuration + public static final class Credentials { + + private String host = "localhost"; + private int port = 3306; + + @Comment("Название базы-данных плагина для миграции.") + private String database = "points"; + private String username = "root"; + private String password = "password"; + } + } + + @Comment({"Эмуляция методов из API других плагинов на донатную валюту.", + "Не влияет на производительность."}) + private Emulation emulation = new Emulation(); + + @Getter + @Accessors(fluent = true) + @Configuration + public static final class Emulation { + + @Comment("Включить ли эмуляцию других плагинов?") + private boolean enabled = false; + + @Comment({"Какая мета будет по-дефолту для эмуляции?", + "Необходимо указать для: PLAYER_POINTS"}) + private String defaultMeta = "RUBIES"; + + @Comment({"Выполнять ли методы из эмуляции всегда принудительно в тихом режиме?", + "Если включено, то не будет логгирования от LuckPerms в консоли."}) + private boolean forcedSilent = false; + + @Comment({"Игнорировать ли методы которые нереализованы для эмуляции?", + "Если включено, то не будет выбрасываться исключение NotEmulatedException."}) + private boolean ignoreNotEmulatedMethods = false; + + @Comment({"Эмулировать список плагинов?", + "Если включено, то будет вносить в список плагинов эмулированный плагин, что позволит ", + "запускаться другим плагинам которые проверяют включен или нет эмулируемый плагин.", + "Влияет на поведение метода PluginManager.isPluginEnabled()"}) + private boolean emulateLookupNames = true; + + @Comment({"Список плагинов которые будут эмулироваться.", + "Доступно: PLAYER_POINTS и COINS_ENGINE"}) + private Set applyTo = Sets.newHashSet( + SupportedEmulation.PLAYER_POINTS, SupportedEmulation.COINS_ENGINE); + } +} diff --git a/src/main/java/me/re4erka/lpmetaplus/action/MetaAction.java b/src/main/java/me/re4erka/lpmetaplus/action/MetaAction.java new file mode 100644 index 0000000..3f7073b --- /dev/null +++ b/src/main/java/me/re4erka/lpmetaplus/action/MetaAction.java @@ -0,0 +1,42 @@ +package me.re4erka.lpmetaplus.action; + +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.experimental.Accessors; +import me.re4erka.lpmetaplus.util.Key; +import org.jetbrains.annotations.NotNull; + +import java.time.Instant; + +@Getter +@Accessors(fluent = true) +@Builder +public class MetaAction { + + private final Type type; + + private final Key key; + private final int count; + + private final Instant timestamp = Instant.now(); + + private static final CharSequence SPACE = " "; + private static final String META_PREFIX = "meta"; + + @NotNull + public String toDescription() { + return String.join(SPACE, META_PREFIX, type.action, key.toString(), Integer.toUnsignedString(count)); + } + + @Getter + @RequiredArgsConstructor + public enum Type { + SET("set"), + GIVE("give"), + TAKE("take"); + + @Accessors(fluent = true) + private final String action; + } +} diff --git a/src/main/java/me/re4erka/lpmetaplus/api/LPMetaPlusAPI.java b/src/main/java/me/re4erka/lpmetaplus/api/LPMetaPlusAPI.java new file mode 100644 index 0000000..bd11198 --- /dev/null +++ b/src/main/java/me/re4erka/lpmetaplus/api/LPMetaPlusAPI.java @@ -0,0 +1,105 @@ +package me.re4erka.lpmetaplus.api; + +import me.re4erka.lpmetaplus.LPMetaPlus; +import me.re4erka.lpmetaplus.session.MetaSession; +import me.re4erka.lpmetaplus.util.Key; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Range; + +import java.util.concurrent.CompletableFuture; + +@SuppressWarnings("unused") +public class LPMetaPlusAPI { + + private static LPMetaPlusAPI instance; + + private final LPMetaPlus plugin; + + @ApiStatus.Internal + private LPMetaPlusAPI(@NotNull LPMetaPlus plugin) { + this.plugin = plugin; + } + + @NotNull + public MetaSession openSession(@NotNull Player player) { + return plugin.getMetaManager().getUser(player); + } + + @NotNull + public CompletableFuture openSession(@NotNull String username) { + return plugin.getMetaManager().findUser(username); + } + + public int getBalance(@NotNull Player player, @NotNull Key key) { + try (MetaSession session = openSession(player)) { + return session.get(key); + } + } + + public void set(@NotNull Player player, @NotNull Key key, @Range(from = 0, to = Integer.MAX_VALUE) int balance, + boolean silent) { + try (MetaSession session = openSession(player)) { + session.edit(editor -> editor.set(key, balance), silent); + } + } + + public void set(@NotNull Player player, @NotNull Key key, @Range(from = 0, to = Integer.MAX_VALUE) int balance) { + set(player, key, balance, false); + } + + public void give(@NotNull Player player, @NotNull Key key, @Range(from = 0, to = Integer.MAX_VALUE) int balance, + boolean silent) { + try (MetaSession session = openSession(player)) { + session.edit(editor -> editor.give(key, balance), silent); + } + } + + public void give(@NotNull Player player, @NotNull Key key, @Range(from = 0, to = Integer.MAX_VALUE) int balance) { + give(player, key, balance, false); + } + + public void take(@NotNull Player player, @NotNull Key key, @Range(from = 0, to = Integer.MAX_VALUE) int balance, + boolean silent) { + try (MetaSession session = openSession(player)) { + session.edit(editor -> editor.give(key, balance), silent); + } + } + + public void take(@NotNull Player player, @NotNull Key key, @Range(from = 0, to = Integer.MAX_VALUE) int balance) { + take(player, key, balance, false); + } + + @NotNull + public static LPMetaPlusAPI getInstance() throws NotRegisteredException { + if (instance == null) { + throw new NotRegisteredException(); + } + + return instance; + } + + @ApiStatus.Internal + public static void register(@NotNull LPMetaPlus plugin) { + instance = new LPMetaPlusAPI(plugin); + } + + @ApiStatus.Internal + public static void unregister() { + instance = null; + } + + public static final class NotRegisteredException extends IllegalStateException { + + private static final String MESSAGE = "The LPMetaPlus API isn't loaded yet!\n" + + "This could be because:\n" + + " a) LPMetaPlus has failed to enable successfully\n" + + " b) Your plugin isn't set to load after LPMetaPlus has (Check if it set as a (soft)depend in plugin.yml or to load: BEFORE in paper-plugin.yml?)\n" + + " c) You are attempting to access LPMetaPlus on plugin construction/before your plugin has enabled."; + + NotRegisteredException() { + super(MESSAGE); + } + } +} diff --git a/src/main/java/me/re4erka/lpmetaplus/command/MetaCommand.java b/src/main/java/me/re4erka/lpmetaplus/command/MetaCommand.java new file mode 100644 index 0000000..0d547cc --- /dev/null +++ b/src/main/java/me/re4erka/lpmetaplus/command/MetaCommand.java @@ -0,0 +1,77 @@ +package me.re4erka.lpmetaplus.command; + +import dev.triumphteam.cmd.core.BaseCommand; +import me.re4erka.lpmetaplus.LPMetaPlus; +import me.re4erka.lpmetaplus.Messages; +import me.re4erka.lpmetaplus.configuration.type.CustomMeta; +import me.re4erka.lpmetaplus.message.placeholder.Placeholders; +import me.re4erka.lpmetaplus.util.UnsignedInts; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public abstract class MetaCommand extends BaseCommand { + + protected final LPMetaPlus lpMetaPlus; + + protected MetaCommand(@NotNull LPMetaPlus lpMetaPlus) { + this.lpMetaPlus = lpMetaPlus; + } + + protected MetaCommand(@NotNull LPMetaPlus lpMetaPlus, @NotNull String command, @NotNull List alias) { + super(command, alias); + this.lpMetaPlus = lpMetaPlus; + } + + protected void ifNotUnsigned(@NotNull CommandSender sender, int count, @NotNull Runnable action) { + if (UnsignedInts.isNot(count)) { + metaMessages().unsignedNotSupported().send(sender); + return; + } + + action.run(); + } + + @NotNull + protected Messages.Command commandMessages() { + return lpMetaPlus.messages().command(); + } + + @NotNull + protected Messages.Meta metaMessages() { + return lpMetaPlus.messages().meta(); + } + + @NotNull + protected Placeholders buildPlaceholders(@NotNull String target, @NotNull CustomMeta meta, int count) { + return builderPlaceholders(meta, count) + .add("target", target) + .build(); + } + + @NotNull + protected Placeholders buildPlaceholders(@NotNull String target, @NotNull CustomMeta meta) { + return builderPlaceholders(meta) + .add("target", target) + .build(); + } + + @NotNull + protected Placeholders buildPlaceholders(@NotNull CustomMeta meta, int balance) { + return builderPlaceholders(meta, balance).build(); + } + + @NotNull + private Placeholders.Builder builderPlaceholders(@NotNull CustomMeta meta, int balance) { + return builderPlaceholders(meta) + .add("balance", Integer.toString(balance)); + } + + @NotNull + private Placeholders.Builder builderPlaceholders(@NotNull CustomMeta meta) { + return Placeholders.builder() + .add("display_name", meta.displayName()) + .add("symbol", meta.symbol()); + } +} diff --git a/src/main/java/me/re4erka/lpmetaplus/command/type/CustomCommand.java b/src/main/java/me/re4erka/lpmetaplus/command/type/CustomCommand.java new file mode 100644 index 0000000..0305256 --- /dev/null +++ b/src/main/java/me/re4erka/lpmetaplus/command/type/CustomCommand.java @@ -0,0 +1,43 @@ +package me.re4erka.lpmetaplus.command.type; + +import dev.triumphteam.cmd.core.annotation.Default; +import me.re4erka.lpmetaplus.LPMetaPlus; +import me.re4erka.lpmetaplus.command.MetaCommand; +import me.re4erka.lpmetaplus.configuration.type.CustomMeta; +import me.re4erka.lpmetaplus.manager.type.MetaManager; +import me.re4erka.lpmetaplus.util.Key; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +@SuppressWarnings("unused") +public class CustomCommand extends MetaCommand { + + private final MetaManager metaManager; + + private final Key key; + private final CustomMeta meta; + + private final String permission; + + public CustomCommand(@NotNull LPMetaPlus lpMetaPlus, + @NotNull String type, @NotNull CustomMeta meta) { + super(lpMetaPlus, type, meta.command().alias()); + this.metaManager = lpMetaPlus.getMetaManager(); + + this.key = Key.of(type); + this.meta = meta; + + this.permission = meta.command().permission(); + } + + @Default + public void onDefault(@NotNull Player player) { + if (permission != null && !player.hasPermission(permission)) { + commandMessages().noPermission().send(player); + return; + } + + final int count = metaManager.getUser(player).get(key); + meta.command().message().send(player, buildPlaceholders(meta, count)); + } +} diff --git a/src/main/java/me/re4erka/lpmetaplus/command/type/MainCommand.java b/src/main/java/me/re4erka/lpmetaplus/command/type/MainCommand.java new file mode 100644 index 0000000..0e3b8f4 --- /dev/null +++ b/src/main/java/me/re4erka/lpmetaplus/command/type/MainCommand.java @@ -0,0 +1,133 @@ +package me.re4erka.lpmetaplus.command.type; + +import dev.triumphteam.cmd.bukkit.annotation.Permission; +import dev.triumphteam.cmd.core.annotation.*; +import dev.triumphteam.cmd.core.flag.Flags; +import me.re4erka.lpmetaplus.LPMetaPlus; +import me.re4erka.lpmetaplus.command.MetaCommand; +import me.re4erka.lpmetaplus.message.placeholder.Placeholders; +import me.re4erka.lpmetaplus.migration.MigrationType; +import me.re4erka.lpmetaplus.migration.Migrator; +import me.re4erka.lpmetaplus.operation.MetaOperation; +import me.re4erka.lpmetaplus.operation.context.MetaOperationContext; +import me.re4erka.lpmetaplus.operation.factory.MetaOperationFactory; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; + +@SuppressWarnings("unused") +@Command(value = "lpmetaplus", alias = {"meta", "metas"}) +public class MainCommand extends MetaCommand { + + public MainCommand(@NotNull LPMetaPlus lpMetaPlus) { + super(lpMetaPlus); + } + + @Default + @Permission("lpmetaplus.command.help") + public void onDefault(@NotNull CommandSender sender) { + onHelp(sender); + } + + @SubCommand("get") + @Permission("lpmetaplus.command.get") + public void onGet(@NotNull CommandSender sender, @NotNull @Suggestion("meta_types") String type, + @NotNull @Suggestion("loaded_user_names") String target) { + final MetaOperationContext context = MetaOperationContext.of(sender, type, target); + final MetaOperation operation = MetaOperationFactory.createReturned(lpMetaPlus, + (meta, count) -> metaMessages().get().send(sender, buildPlaceholders(target, meta, count))); + + operation.execute(context); + } + + @SubCommand("set") + @Permission("lpmetaplus.command.set") + @CommandFlags({@Flag(longFlag = "silent", flag = "s")}) + public void onSet(@NotNull CommandSender sender, @NotNull @Suggestion("meta_types") String type, + @Suggestion("any_count") int count, @NotNull @Suggestion("loaded_user_names") String target, + @NotNull Flags flags) { + ifNotUnsigned(sender, count, () -> { + final MetaOperationContext context = MetaOperationContext.of(sender, type, target); + final MetaOperation operation = MetaOperationFactory.createEditable(lpMetaPlus, + (key, editor) -> editor.set(key, count), + (meta) -> metaMessages().set().send(sender, buildPlaceholders(target, meta, count))); + + operation.execute(context, flags.hasFlag("s")); + }); + } + + @SubCommand("give") + @Permission("lpmetaplus.command.give") + @CommandFlags({@Flag(longFlag = "silent", flag = "s")}) + public void onGive(@NotNull CommandSender sender, @NotNull @Suggestion("meta_types") String type, + @Suggestion("any_count") int count, @NotNull @Suggestion("loaded_user_names") String target, + @NotNull Flags flags) { + ifNotUnsigned(sender, count, () -> { + final MetaOperationContext context = MetaOperationContext.of(sender, type, target); + final MetaOperation operation = MetaOperationFactory.createEditable(lpMetaPlus, + (key, editor) -> editor.give(key, count), + (meta) -> metaMessages().given().send(sender, buildPlaceholders(target, meta, count))); + + operation.execute(context, flags.hasFlag("s")); + }); + } + + @SubCommand("take") + @Permission("lpmetaplus.command.take") + @CommandFlags({@Flag(longFlag = "silent", flag = "s")}) + public void onTake(@NotNull CommandSender sender, @NotNull @Suggestion("meta_types") String type, + @Suggestion("any_count") int count, @NotNull @Suggestion("loaded_user_names") String target, + @NotNull Flags flags) { + ifNotUnsigned(sender, count, () -> { + final MetaOperationContext context = MetaOperationContext.of(sender, type, target); + final MetaOperation operation = MetaOperationFactory.createEditable(lpMetaPlus, + (key, editor) -> editor.take(key, count), + (meta) -> metaMessages().taken().send(sender, buildPlaceholders(target, meta, count))); + + operation.execute(context, flags.hasFlag("s")); + }); + } + + @SubCommand("reset") + @Permission("lpmetaplus.command.reset") + @CommandFlags({@Flag(longFlag = "silent", flag = "s")}) + public void onReset(@NotNull CommandSender sender, @NotNull @Suggestion("meta_types") String type, + @NotNull @Suggestion("loaded_user_names") String target, @NotNull Flags flags) { + final MetaOperationContext context = MetaOperationContext.of(sender, type, target); + final MetaOperation operation = MetaOperationFactory.createEditable(lpMetaPlus, + (key, editor) -> editor.remove(key), + (meta) -> metaMessages().reset().send(sender, buildPlaceholders(target, meta))); + + operation.execute(context, flags.hasFlag("s")); + } + + @SubCommand("migrate") + @Permission("lpmetaplus.command.migrate") + public void onMigrate(@NotNull CommandSender sender, + @NotNull MigrationType migrationType, + @NotNull Migrator.DatabaseType databaseType) { + commandMessages().migrationInProgress().send(sender, Placeholders.single("name", migrationType.name())); + migrationType.initialize(lpMetaPlus) + .migrate(databaseType) + .thenAccept(result -> { + if (result.isFailed()) { + commandMessages().migrationFailed().send(sender, result.toPlaceholders(migrationType)); + } else { + commandMessages().migrated().send(sender, result.toPlaceholders(migrationType)); + } + }); + } + + @SubCommand("reload") + @Permission("lpmetaplus.command.reload") + public void onReload(@NotNull CommandSender sender) { + lpMetaPlus.reload(); + commandMessages().reloaded().send(sender); + } + + @SubCommand("help") + @Permission("lpmetaplus.command.help") + public void onHelp(@NotNull CommandSender sender) { + commandMessages().help() + .forEach(line -> line.send(sender)); + } +} diff --git a/src/main/java/me/re4erka/lpmetaplus/configuration/ConfigurationMetas.java b/src/main/java/me/re4erka/lpmetaplus/configuration/ConfigurationMetas.java new file mode 100644 index 0000000..a2097c0 --- /dev/null +++ b/src/main/java/me/re4erka/lpmetaplus/configuration/ConfigurationMetas.java @@ -0,0 +1,84 @@ +package me.re4erka.lpmetaplus.configuration; + +import com.google.common.collect.Lists; +import de.exlll.configlib.Configuration; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import me.re4erka.lpmetaplus.configuration.type.CustomMeta; +import me.re4erka.lpmetaplus.message.Message; +import me.re4erka.lpmetaplus.util.Key; +import me.re4erka.lpmetaplus.util.SortedMaps; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Unmodifiable; + +import java.util.*; + +@Getter +@Accessors(fluent = true) +@SuppressWarnings("FieldMayBeFinal") +@Configuration +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class ConfigurationMetas { + + private Map types = SortedMaps.of( + "RUBIES", CustomMeta.builder() + .displayName("&cРубины") + .symbol('◆') + .command(CustomMeta.Command.builder() + .enabled(true) + .permission("lpmetaplus.command.rubies") + .message(Message.of("Ваш баланс: &c%balance% %symbol%")) + .alias(Lists.newArrayList("ruby", "рубины")) + .build()) + .build(), + "RUBLES", CustomMeta.builder() + .displayName("&aРубли") + .symbol('₽') + .command(CustomMeta.Command.builder() + .enabled(true) + .message(Message.of("Ваш баланс: &a%balance% %symbol%")) + .alias(Lists.newArrayList("ruble", "рубли")) + .build()) + .build(), + "FLOWERS", CustomMeta.builder() + .displayName("&eЦветочки") + .symbol('❀') + .defaultContexts(SortedMaps.of("world", "spawn")) + .build() + ); + + @NotNull + public Optional type(@NotNull String type) { + return Optional.ofNullable(types.get( + type.toUpperCase(Locale.ROOT) + )); + } + + @NotNull + public CustomMeta type(@NotNull Key key) { + return types.get(key.toString()); + } + + public Key getOrThrow(@NotNull String type) { + final Key key = Key.of(type); + if (contains(key)) { + return key; + } + + throw new IllegalArgumentException("The type of meta was not found!"); + } + + public boolean contains(@NotNull Key key) { + return types.containsKey(key.toString()); + } + + @NotNull + @Unmodifiable + public List names() { + return Collections.unmodifiableList( + new ArrayList<>(types.keySet()) + ); + } +} \ No newline at end of file diff --git a/src/main/java/me/re4erka/lpmetaplus/configuration/type/CustomMeta.java b/src/main/java/me/re4erka/lpmetaplus/configuration/type/CustomMeta.java new file mode 100644 index 0000000..d28198a --- /dev/null +++ b/src/main/java/me/re4erka/lpmetaplus/configuration/type/CustomMeta.java @@ -0,0 +1,73 @@ +package me.re4erka.lpmetaplus.configuration.type; + +import de.exlll.configlib.Configuration; +import lombok.*; +import lombok.experimental.Accessors; +import me.re4erka.lpmetaplus.message.Message; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +@Builder +@Getter +@Accessors(fluent = true) +@SuppressWarnings("FieldMayBeFinal") +@Configuration +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public final class CustomMeta { + + @Nullable + @Builder.Default + private String displayName = null; + @Builder.Default + private int defaultValue = 0; + @Nullable + @Builder.Default + private Character symbol = null; + + @Getter(value = AccessLevel.NONE) + private Map defaultContexts = null; + + @NotNull + public String defaultValueToString() { + return Integer.toString(defaultValue); + } + + @NotNull + public Map defaultContexts() { + return defaultContexts == null + ? Collections.emptyMap() + : defaultContexts; + } + + public boolean isCommandEnabled() { + return command != null && command.enabled; + } + + private Command command = new Command(); + + @Getter + @Accessors(fluent = true) + @Builder + @Configuration + @NoArgsConstructor(access = AccessLevel.PRIVATE) + @AllArgsConstructor(access = AccessLevel.PRIVATE) + public static final class Command { + + @Builder.Default + private boolean enabled = false; + @Nullable + @Builder.Default + private String permission = null; + @NotNull + @Builder.Default + private Message message = Message.empty(); + @NotNull + @Builder.Default + private List alias = Collections.emptyList(); + } +} diff --git a/src/main/java/me/re4erka/lpmetaplus/manager/LuckPermsProviderManager.java b/src/main/java/me/re4erka/lpmetaplus/manager/LuckPermsProviderManager.java new file mode 100644 index 0000000..3c6734b --- /dev/null +++ b/src/main/java/me/re4erka/lpmetaplus/manager/LuckPermsProviderManager.java @@ -0,0 +1,51 @@ +package me.re4erka.lpmetaplus.manager; + +import me.re4erka.lpmetaplus.LPMetaPlus; +import net.luckperms.api.LuckPerms; +import net.luckperms.api.LuckPermsProvider; +import net.luckperms.api.actionlog.ActionLogger; +import net.luckperms.api.model.group.GroupManager; +import net.luckperms.api.model.user.UserManager; +import org.jetbrains.annotations.NotNull; + +public abstract class LuckPermsProviderManager { + + protected final LPMetaPlus lpMetaPlus; + protected final LuckPerms luckPerms; + + protected final UserManager userManager; + protected final GroupManager groupManager; + + protected final ActionLogger actionLogger; + + public static final String CONTEXT_KEY = "meta-plus"; + protected static final String DEFAULT_GROUP_NAME = "default"; + + protected LuckPermsProviderManager(@NotNull LPMetaPlus lpMetaPlus) { + this.lpMetaPlus = lpMetaPlus; + + throwIfLuckPermsProviderClassNotFound(); + this.luckPerms = LuckPermsProvider.get(); + + this.userManager = luckPerms.getUserManager(); + this.groupManager = luckPerms.getGroupManager(); + + this.actionLogger = luckPerms.getActionLogger(); + } + + private void throwIfLuckPermsProviderClassNotFound() { + try { + Class.forName("net.luckperms.api.LuckPermsProvider"); + } catch (ClassNotFoundException exception) { + lpMetaPlus.logError("The class LuckPermsProvider was not found! " + + "Probably an outdated version of LuckPerms is used, " + + "please install version 5.0 or higher."); + throw new RuntimeException(exception); + } catch (IllegalStateException exception) { + lpMetaPlus.logError("The LuckPermsProvider class has not been loaded! " + + "Probably the LuckPerms plugin is not specified as a dependency " + + "in the plugin.yml or paper-plugin.yml file."); + throw new RuntimeException(exception); + } + } +} diff --git a/src/main/java/me/re4erka/lpmetaplus/manager/type/GroupManager.java b/src/main/java/me/re4erka/lpmetaplus/manager/type/GroupManager.java new file mode 100644 index 0000000..014368b --- /dev/null +++ b/src/main/java/me/re4erka/lpmetaplus/manager/type/GroupManager.java @@ -0,0 +1,89 @@ +package me.re4erka.lpmetaplus.manager.type; + +import com.google.common.base.Stopwatch; +import me.re4erka.lpmetaplus.LPMetaPlus; +import me.re4erka.lpmetaplus.configuration.ConfigurationMetas; +import me.re4erka.lpmetaplus.configuration.type.CustomMeta; +import me.re4erka.lpmetaplus.manager.LuckPermsProviderManager; +import net.luckperms.api.context.ImmutableContextSet; +import net.luckperms.api.model.group.Group; +import net.luckperms.api.node.NodeType; +import net.luckperms.api.node.types.MetaNode; +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.stream.Collectors; + +public class GroupManager extends LuckPermsProviderManager { + + public GroupManager(@NotNull LPMetaPlus lpMetaPlus) { + super(lpMetaPlus); + } + + public void load(@NotNull ConfigurationMetas metas) { + lpMetaPlus.logInfo("Setting default metadata for group 'default'..."); + final Stopwatch stopwatch = Stopwatch.createStarted(); + + groupManager.loadGroup(DEFAULT_GROUP_NAME) + .thenAccept(optionalGroup -> { + final Group group = optionalGroup.orElseThrow(this::throwGroupNotFound); + loadDefaultGroupValues(group, metas.types()); + clearRemovedMetadata(group, metas.names()); + + groupManager.saveGroup(group); + }).join(); + + logResult(metas.types(), stopwatch); + } + + private void loadDefaultGroupValues(@NotNull Group group, @NotNull Map metaMap) { + for (Map.Entry entry : metaMap.entrySet()) { + final CustomMeta meta = entry.getValue(); + + final ImmutableContextSet.Builder contextBuilder = newContextBuilder(); + meta.defaultContexts().forEach(contextBuilder::add); + + final MetaNode node = MetaNode.builder() + .key(entry.getKey().toLowerCase(Locale.ROOT)) + .value(meta.defaultValueToString()) + .context(contextBuilder.build()) + .build(); + + group.data().add(node); + } + } + + private void clearRemovedMetadata(@NotNull Group group, @NotNull List names) { + final Set nodes = group.getNodes(NodeType.META).stream() + .filter(node -> node.getContexts().contains(CONTEXT_KEY, Boolean.TRUE.toString())) + .filter(node -> !names.contains(node.getMetaKey().toUpperCase(Locale.ROOT))) + .collect(Collectors.toSet()); + + if (!nodes.isEmpty()) { + nodes.forEach(node -> group.data().remove(node)); + lpMetaPlus.getLogger().log(Level.INFO, "Removed metadata was found, cleared {0}", nodes.size()); + } + } + + @NotNull + private RuntimeException throwGroupNotFound() { + return new IllegalStateException("Group 'default' cannot be null!"); + } + + @NotNull + private ImmutableContextSet.Builder newContextBuilder() { + return ImmutableContextSet.builder() + .add(CONTEXT_KEY, Boolean.TRUE.toString()); + } + + private void logResult(@NotNull Map metaMap, @NotNull Stopwatch stopwatch) { + final StringJoiner joiner = new StringJoiner(", "); + metaMap.forEach((key, value) -> joiner.add(key + "=" + value.defaultValue())); + + lpMetaPlus.getLogger().log(Level.INFO, + "Metadata set for group 'default': " + joiner + " in {0}ms", + stopwatch.elapsed(TimeUnit.MILLISECONDS)); + } +} diff --git a/src/main/java/me/re4erka/lpmetaplus/manager/type/MetaManager.java b/src/main/java/me/re4erka/lpmetaplus/manager/type/MetaManager.java new file mode 100644 index 0000000..59d021e --- /dev/null +++ b/src/main/java/me/re4erka/lpmetaplus/manager/type/MetaManager.java @@ -0,0 +1,145 @@ +package me.re4erka.lpmetaplus.manager.type; + +import me.re4erka.lpmetaplus.LPMetaPlus; +import me.re4erka.lpmetaplus.manager.LuckPermsProviderManager; +import me.re4erka.lpmetaplus.session.MetaSession; +import me.re4erka.lpmetaplus.util.OfflineUUID; +import net.luckperms.api.event.EventBus; +import net.luckperms.api.event.node.NodeClearEvent; +import net.luckperms.api.event.node.NodeMutateEvent; +import net.luckperms.api.event.node.NodeRemoveEvent; +import net.luckperms.api.model.data.DataType; +import net.luckperms.api.model.group.Group; +import net.luckperms.api.model.user.User; +import net.luckperms.api.node.Node; +import org.bukkit.OfflinePlayer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Level; +import java.util.stream.Collectors; + +public class MetaManager extends LuckPermsProviderManager { + + private final Group defaultGroup; + + public MetaManager(@NotNull LPMetaPlus lpMetaPlus) { + super(lpMetaPlus); + + this.defaultGroup = groupManager.getGroup(DEFAULT_GROUP_NAME); + throwIfDefaultGroupNotFound(defaultGroup); + } + + @NotNull + @SuppressWarnings("ConstantConditions") + public MetaSession getUser(@NotNull OfflinePlayer player) { + return getUser(player.getUniqueId()); + } + + @NotNull + @SuppressWarnings("ConstantConditions") + public MetaSession getUser(@NotNull UUID uuid) { + final User cachedUser = userManager.getUser(uuid); + throwIfUserIsNull(cachedUser); + return openSession(cachedUser, false); + } + + @NotNull + public CompletableFuture findUser(@NotNull UUID uuid, @NotNull String username) { + final User cachedUser = userManager.getUser(uuid); + if (cachedUser == null) { + return CompletableFuture.supplyAsync(() -> openSession(loadUser(uuid, username), true)); + } else { + return CompletableFuture.completedFuture(openSession(cachedUser, false)); + } + } + + @NotNull + public CompletableFuture findUser(@NotNull String username) { + final User cachedUser = userManager.getUser(username); + if (cachedUser == null) { + return CompletableFuture.supplyAsync(() -> openSession(lookup(username), true)); + } else { + return CompletableFuture.completedFuture(openSession(cachedUser, false)); + } + } + + public void registerWarningEvents() { + final EventBus eventBus = luckPerms.getEventBus(); + eventBus.subscribe(lpMetaPlus, NodeRemoveEvent.class, this::warnIfRemovedPlusMetaFromGroup); + eventBus.subscribe(lpMetaPlus, NodeClearEvent.class, this::warnIfRemovedPlusMetaFromGroup); + } + + @NotNull + public MetaSession openSession(@NotNull User user, boolean lookup) { + return new MetaSession(lpMetaPlus, userManager, actionLogger, defaultGroup, user, lookup); + } + + private void warnIfRemovedPlusMetaFromGroup(@NotNull NodeMutateEvent nodeMutateEvent) { + if (nodeMutateEvent.isGroup() && nodeMutateEvent.getDataType() == DataType.NORMAL) { + final Set nodesBefore = nodeMutateEvent.getDataBefore().stream() + .filter(node -> node.getContexts().containsKey(LuckPermsProviderManager.CONTEXT_KEY)) + .collect(Collectors.toSet()); + + if (!nodesBefore.isEmpty()) { + final Set nodesAfter = nodeMutateEvent.getDataAfter().stream() + .filter(node -> node.getContexts().containsKey(LuckPermsProviderManager.CONTEXT_KEY)) + .collect(Collectors.toSet()); + + if (nodesBefore.size() != nodesAfter.size()) { + final Set removedNodes = nodesBefore.stream() + .filter(nodeBefore -> nodesAfter.stream() + .noneMatch(nodeAfter -> nodeAfter.getKey().equals(nodeBefore.getKey()))) + .collect(Collectors.toSet()); + logWarning(removedNodes); + } + } + } + } + + private void logWarning(@NotNull Set removedNodes) { + final StringJoiner joiner = new StringJoiner("', '"); + removedNodes.forEach(node -> joiner.add(node.getKey())); + + lpMetaPlus.getLogger().log(Level.WARNING, "Probably the meta that is used " + + "by LPMetaPlus plugin has been unset/cleared, without it the plugin may not work properly. " + + "Please return the meta ['" + joiner + "'] or restart the plugin if you are not sure."); + } + + @NotNull + private User lookup(@NotNull String username) { + UUID uuid = userManager.lookupUniqueId(username).join(); + if (uuid == null) { + uuid = OfflineUUID.fromName(username); + } + + return loadUser(uuid, username); + } + + @NotNull + private User loadUser(@NotNull UUID uuid, @NotNull String username) { + final User user = userManager.loadUser(uuid, username).join(); + user.auditTemporaryNodes(); + + return user; + } + + private void throwIfDefaultGroupNotFound(@Nullable Group group) { + if (group == null) { + lpMetaPlus.logError("When trying to get the default group, a null was get, " + + "which should not be the case."); + throw new IllegalStateException("Default group cannot be null!"); + } + } + + private void throwIfUserIsNull(@Nullable User user) { + if (user == null) { + lpMetaPlus.logError("An error occurred when trying to get a user by UUID. " + + "The method being called assumes that the player being retrieved will be online, " + + "maybe the problem is related to this"); + throw new NullPointerException("A user cannot be null!"); + } + } +} diff --git a/src/main/java/me/re4erka/lpmetaplus/message/Message.java b/src/main/java/me/re4erka/lpmetaplus/message/Message.java new file mode 100644 index 0000000..5b92d0d --- /dev/null +++ b/src/main/java/me/re4erka/lpmetaplus/message/Message.java @@ -0,0 +1,72 @@ +package me.re4erka.lpmetaplus.message; + +import me.re4erka.lpmetaplus.message.placeholder.Placeholders; +import me.re4erka.lpmetaplus.util.Formatter; +import org.apache.commons.lang.StringUtils; +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; + +public final class Message { + + private final String text; + + private static final Message EMPTY = Message.of(StringUtils.EMPTY); + + private Message(@NotNull String text) { + this.text = text; + } + + public void send(@NotNull CommandSender sender) { + if (text.isEmpty()) { + return; + } + + send(sender, text); + } + + public void send(@NotNull CommandSender sender, @NotNull Placeholders placeholders) { + if (text.isEmpty()) { + return; + } + + send(sender, placeholders.process(text)); + } + + public boolean isEmpty() { + return this == EMPTY || text.isEmpty(); + } + + private void send(@NotNull CommandSender sender, @NotNull String text) { + sender.sendMessage(isConsoleSender(sender) ? Formatter.strip(text) : Formatter.format(text)); + } + + private boolean isConsoleSender(@NotNull CommandSender sender) { + return Bukkit.getConsoleSender() == sender; + } + + @NotNull + public static Message of(@NotNull String text) { + return new Message(text); + } + + @NotNull + public static Message empty() { + return EMPTY; + } + + public static class Serializer implements de.exlll.configlib.Serializer { + + @NotNull + @Override + public String serialize(@NotNull Message message) { + return message.text; + } + + @NotNull + @Override + public Message deserialize(String value) { + return new Message(value); + } + } +} diff --git a/src/main/java/me/re4erka/lpmetaplus/message/placeholder/Placeholder.java b/src/main/java/me/re4erka/lpmetaplus/message/placeholder/Placeholder.java new file mode 100644 index 0000000..016a817 --- /dev/null +++ b/src/main/java/me/re4erka/lpmetaplus/message/placeholder/Placeholder.java @@ -0,0 +1,28 @@ +package me.re4erka.lpmetaplus.message.placeholder; + +import lombok.Getter; +import lombok.experimental.Accessors; +import org.jetbrains.annotations.NotNull; + +@Getter +@Accessors(fluent = true) +public final class Placeholder { + + private final String search; + private final String replacement; + + private Placeholder(@NotNull String search, @NotNull String replacement) { + this.search = convert(search); + this.replacement = replacement; + } + + @NotNull + private String convert(@NotNull String raw) { + return '%' + raw + '%'; + } + + @NotNull + public static Placeholder of(@NotNull String search, @NotNull String replacement) { + return new Placeholder(search, replacement); + } +} diff --git a/src/main/java/me/re4erka/lpmetaplus/message/placeholder/Placeholders.java b/src/main/java/me/re4erka/lpmetaplus/message/placeholder/Placeholders.java new file mode 100644 index 0000000..cf1e9cf --- /dev/null +++ b/src/main/java/me/re4erka/lpmetaplus/message/placeholder/Placeholders.java @@ -0,0 +1,85 @@ +package me.re4erka.lpmetaplus.message.placeholder; + +import com.google.common.collect.Lists; +import org.apache.commons.lang.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +public final class Placeholders { + + private final List placeholders; + + private Placeholders(@NotNull List placeholders) { + this.placeholders = placeholders; + } + + @NotNull + public String process(@NotNull String text) { + String processedText = text; + for (Placeholder placeholder : placeholders) { + processedText = processedText.replace(placeholder.search(), placeholder.replacement()); + } + + return processedText; + } + + @NotNull + public static Placeholders single(@NotNull String search, @NotNull String replacement) { + return new Placeholders( + Lists.newArrayList(Placeholder.of(search, replacement)) + ); + } + + @NotNull + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private final List placeholders = new ArrayList<>(); + + @NotNull + public Builder add(@NotNull String search, @Nullable String replacement) { + placeholders.add(Placeholder.of(search, notNull(replacement))); + return this; + } + + @NotNull + public Builder add(@NotNull String search, @Nullable Character replacement) { + placeholders.add(Placeholder.of(search, notNullToString(replacement))); + return this; + } + + @NotNull + public Builder add(@NotNull String search, int replacement) { + placeholders.add(Placeholder.of(search, Integer.toString(replacement))); + return this; + } + + @NotNull + public Builder add(@NotNull String search, long replacement) { + placeholders.add(Placeholder.of(search, Long.toString(replacement))); + return this; + } + + @NotNull + public Placeholders build() { + return new Placeholders( + Collections.unmodifiableList(placeholders) + ); + } + + @NotNull + private String notNull(@Nullable String value) { + return value == null ? StringUtils.EMPTY : value; + } + + @NotNull + private String notNullToString(@Nullable Character value) { + return value == null ? StringUtils.EMPTY : Character.toString(value); + } + } +} diff --git a/src/main/java/me/re4erka/lpmetaplus/migration/MigrationType.java b/src/main/java/me/re4erka/lpmetaplus/migration/MigrationType.java new file mode 100644 index 0000000..06da91b --- /dev/null +++ b/src/main/java/me/re4erka/lpmetaplus/migration/MigrationType.java @@ -0,0 +1,20 @@ +package me.re4erka.lpmetaplus.migration; + +import lombok.RequiredArgsConstructor; +import me.re4erka.lpmetaplus.LPMetaPlus; +import me.re4erka.lpmetaplus.migration.type.PlayerPointsMigrator; +import org.jetbrains.annotations.NotNull; + +import java.util.function.Function; + +@SuppressWarnings("unused") +@RequiredArgsConstructor +public enum MigrationType { + PLAYER_POINTS(PlayerPointsMigrator::new); + + private final Function initializer; + + public Migrator initialize(@NotNull LPMetaPlus lpMetaPlus) { + return initializer.apply(lpMetaPlus); + } +} diff --git a/src/main/java/me/re4erka/lpmetaplus/migration/Migrator.java b/src/main/java/me/re4erka/lpmetaplus/migration/Migrator.java new file mode 100644 index 0000000..e3cf043 --- /dev/null +++ b/src/main/java/me/re4erka/lpmetaplus/migration/Migrator.java @@ -0,0 +1,71 @@ +package me.re4erka.lpmetaplus.migration; + +import me.re4erka.lpmetaplus.LPMetaPlus; +import me.re4erka.lpmetaplus.Settings; +import me.re4erka.lpmetaplus.manager.type.MetaManager; +import me.re4erka.lpmetaplus.migration.data.MigrationData; +import me.re4erka.lpmetaplus.migration.result.MigrationResult; +import me.re4erka.lpmetaplus.session.MetaSession; +import me.re4erka.lpmetaplus.util.Key; +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.nio.file.Path; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +public abstract class Migrator { + + protected final LPMetaPlus lpMetaPlus; + + protected final String name; + protected final Plugin plugin; + + protected final Key defaultType; + protected final Settings.Migration.Credentials credentials; + + protected Migrator(@NotNull String name, + @NotNull LPMetaPlus lpMetaPlus) { + this.lpMetaPlus = lpMetaPlus; + + this.name = name; + this.plugin = Bukkit.getPluginManager().getPlugin(name); + throwIfPluginIsNull(plugin); + + final Settings.Migration migration = lpMetaPlus.settings().migration(); + this.defaultType = lpMetaPlus.metas().getOrThrow(migration.defaultType()); + this.credentials = migration.credentials(); + } + + public abstract CompletableFuture migrate(@NotNull DatabaseType type); + + protected Path getDatabaseFilePath(@NotNull String fileName) { + return plugin.getDataFolder().toPath().resolve(fileName + ".db"); + } + + protected void migrateAll(@NotNull Set dataList) { + final MetaManager metaManager = lpMetaPlus.getMetaManager(); + for (MigrationData data : dataList) { + metaManager.findUser(data.uuid(), data.username()) + .thenAccept(session -> { + try (MetaSession ignored = session) { + session.edit(editor -> editor.set(defaultType, data.balance()), true); + } + }).join(); + } + } + + private void throwIfPluginIsNull(@Nullable Plugin plugin) { + if (plugin == null) { + lpMetaPlus.logError(name + " plugin was not found! It may be disabled or not installed, " + + "but it is required to migrate."); + throw new NullPointerException("The plugin cannot be null!"); + } + } + + public enum DatabaseType { + SQLITE, MYSQL + } +} diff --git a/src/main/java/me/re4erka/lpmetaplus/migration/data/MigrationData.java b/src/main/java/me/re4erka/lpmetaplus/migration/data/MigrationData.java new file mode 100644 index 0000000..cf03090 --- /dev/null +++ b/src/main/java/me/re4erka/lpmetaplus/migration/data/MigrationData.java @@ -0,0 +1,18 @@ +package me.re4erka.lpmetaplus.migration.data; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.experimental.Accessors; + +import java.util.UUID; + +@Getter +@Accessors(fluent = true) +@RequiredArgsConstructor(staticName = "of") +public class MigrationData { + + private final UUID uuid; + private final String username; + + private final int balance; +} diff --git a/src/main/java/me/re4erka/lpmetaplus/migration/result/MigrationResult.java b/src/main/java/me/re4erka/lpmetaplus/migration/result/MigrationResult.java new file mode 100644 index 0000000..ecdd6a4 --- /dev/null +++ b/src/main/java/me/re4erka/lpmetaplus/migration/result/MigrationResult.java @@ -0,0 +1,33 @@ +package me.re4erka.lpmetaplus.migration.result; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.experimental.Accessors; +import me.re4erka.lpmetaplus.message.placeholder.Placeholders; +import me.re4erka.lpmetaplus.migration.MigrationType; +import org.jetbrains.annotations.NotNull; + +@Getter +@Accessors(fluent = true) +@RequiredArgsConstructor(staticName = "of") +public class MigrationResult { + + private final int playersMigrated; + private final long tookToMillis; + + public boolean isFailed() { + return playersMigrated == 0; + } + + public Placeholders toPlaceholders(@NotNull MigrationType type) { + return Placeholders.builder() + .add("count", playersMigrated) + .add("took", tookToMillis) + .add("name", type.name()) + .build(); + } + + public static MigrationResult failed(long millis) { + return new MigrationResult(0, millis); + } +} diff --git a/src/main/java/me/re4erka/lpmetaplus/migration/type/PlayerPointsMigrator.java b/src/main/java/me/re4erka/lpmetaplus/migration/type/PlayerPointsMigrator.java new file mode 100644 index 0000000..17baf9c --- /dev/null +++ b/src/main/java/me/re4erka/lpmetaplus/migration/type/PlayerPointsMigrator.java @@ -0,0 +1,83 @@ +package me.re4erka.lpmetaplus.migration.type; + +import com.google.common.base.Stopwatch; +import com.google.common.collect.Sets; +import com.zaxxer.hikari.HikariDataSource; +import me.re4erka.lpmetaplus.LPMetaPlus; +import me.re4erka.lpmetaplus.migration.Migrator; +import me.re4erka.lpmetaplus.migration.data.MigrationData; +import me.re4erka.lpmetaplus.migration.result.MigrationResult; +import org.jetbrains.annotations.NotNull; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.Locale; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +public class PlayerPointsMigrator extends Migrator { + + public PlayerPointsMigrator(@NotNull LPMetaPlus lpMetaPlus) { + super("PlayerPoints", lpMetaPlus); + } + + @Override + public CompletableFuture migrate(@NotNull DatabaseType type) { + return CompletableFuture.supplyAsync(() -> { + final Stopwatch stopwatch = Stopwatch.createStarted(); + final String jdbcUrl = type == DatabaseType.SQLITE + ? "jdbc:sqlite:" + getDatabaseFilePath(name.toLowerCase(Locale.ROOT)) + : "jdbc:mysql://" + credentials.host() + ":" + credentials.port() + "/" + credentials.database(); + + try (HikariDataSource connectionPool = new HikariDataSource()) { + connectionPool.setJdbcUrl(jdbcUrl); + connectionPool.setUsername(credentials.username()); + connectionPool.setPassword(credentials.password()); + connectionPool.setPoolName("lpmetaplus_migrator_pool"); + + final Set migrationDataSet = Sets.newHashSet(); + try (Connection connection = connectionPool.getConnection()) { + final String query = "SELECT p.uuid, p.points, u.username FROM `playerpoints_points` p INNER " + + "JOIN `playerpoints_username_cache` u ON p.uuid = u.uuid;"; + + try (PreparedStatement statement = connection.prepareStatement(query)) { + try (ResultSet result = statement.executeQuery()) { + + int playersMigrated = 0; + while (result.next()) { + final String uuid = result.getString("uuid"); + final String username = result.getString("username"); + final int points = result.getInt("points"); + + final MigrationData data = MigrationData.of( + UUID.fromString(uuid), username, points + ); + + migrationDataSet.add(data); + + playersMigrated++; + if (playersMigrated % 100 == 0) { + lpMetaPlus.logInfo("Downloaded " + name + + " data for " + playersMigrated + " players..."); + } + } + + migrateAll(migrationDataSet); + return MigrationResult.of( + migrationDataSet.size(), + stopwatch.elapsed(TimeUnit.MILLISECONDS) + ); + } + } + } + } catch (Throwable exception) { + lpMetaPlus.logError("An error occurred when migrating from " + name + " with " + type.name() + ". " + + "The connection credentials may have been entered incorrectly.", exception); + return MigrationResult.failed(stopwatch.elapsed(TimeUnit.MILLISECONDS)); + } + }); + } +} diff --git a/src/main/java/me/re4erka/lpmetaplus/operation/AbstractMetaOperation.java b/src/main/java/me/re4erka/lpmetaplus/operation/AbstractMetaOperation.java new file mode 100644 index 0000000..66cebd8 --- /dev/null +++ b/src/main/java/me/re4erka/lpmetaplus/operation/AbstractMetaOperation.java @@ -0,0 +1,83 @@ +package me.re4erka.lpmetaplus.operation; + +import me.re4erka.lpmetaplus.LPMetaPlus; +import me.re4erka.lpmetaplus.configuration.type.CustomMeta; +import me.re4erka.lpmetaplus.operation.context.MetaOperationContext; +import me.re4erka.lpmetaplus.session.MetaSession; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Supplier; + +public abstract class AbstractMetaOperation implements MetaOperation { + + protected final LPMetaPlus lpMetaPlus; + + protected AbstractMetaOperation(@NotNull LPMetaPlus lpMetaPlus) { + this.lpMetaPlus = lpMetaPlus; + } + + protected void ifMetaPresent(@NotNull MetaOperationContext context, @NotNull Consumer action) { + final Optional optionalMeta = lpMetaPlus.metas().type(context.type()); + if (!optionalMeta.isPresent()) { + lpMetaPlus.messages().meta().notFound().send(context.sender()); + return; + } + + action.accept(optionalMeta.get()); + } + + protected void tryRunOperation(@NotNull MetaSession session, @NotNull Runnable action) { + try (MetaSession ignored = session) { + action.run(); + } catch (Exception exception) { + if (session.isLookup()) { + logCleanupError(); + } + throw exception; + } + } + + protected int tryRunOperationAndReturn(@NotNull MetaSession session, @NotNull Supplier returnable) { + try (MetaSession ignored = session) { + return returnable.get(); + } catch (Exception exception) { + if (session.isLookup()) { + logCleanupError(); + } + throw exception; + } + } + + protected void runIfNotSilent(@NotNull Runnable action, boolean silent) { + if (!silent) { + action.run(); + } + } + + @Nullable + protected Void throwException(@NotNull MetaOperationContext context, @NotNull Throwable throwable) { + logError(throwable); + sendError(context.sender()); + return null; + } + + private void logCleanupError() { + lpMetaPlus.logError("There was an error in the metadata operations session, " + + "the user obtained by lookup was cleaned up to prevent memory leaks."); + } + + private void sendError(@NotNull CommandSender sender) { + sender.sendMessage(ChatColor.COLOR_CHAR + ChatColor.RED.toString() + + "При выполнении операции с мета-данной произошла ошибка. " + + "Проверьте консоль, чтобы узнать подробнее."); + } + + private void logError(@NotNull Throwable throwable) { + lpMetaPlus.logError("An error occurred when the operation was running with metadata", throwable); + } +} diff --git a/src/main/java/me/re4erka/lpmetaplus/operation/MetaOperation.java b/src/main/java/me/re4erka/lpmetaplus/operation/MetaOperation.java new file mode 100644 index 0000000..4687642 --- /dev/null +++ b/src/main/java/me/re4erka/lpmetaplus/operation/MetaOperation.java @@ -0,0 +1,14 @@ +package me.re4erka.lpmetaplus.operation; + +import me.re4erka.lpmetaplus.operation.context.MetaOperationContext; +import org.jetbrains.annotations.NotNull; + +@FunctionalInterface +public interface MetaOperation { + + void execute(@NotNull MetaOperationContext context, boolean silent); + + default void execute(@NotNull MetaOperationContext context) { + execute(context, false); + } +} diff --git a/src/main/java/me/re4erka/lpmetaplus/operation/context/MetaOperationContext.java b/src/main/java/me/re4erka/lpmetaplus/operation/context/MetaOperationContext.java new file mode 100644 index 0000000..63ccd29 --- /dev/null +++ b/src/main/java/me/re4erka/lpmetaplus/operation/context/MetaOperationContext.java @@ -0,0 +1,32 @@ +package me.re4erka.lpmetaplus.operation.context; + +import lombok.Getter; +import lombok.experimental.Accessors; +import me.re4erka.lpmetaplus.util.Key; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; + +@Getter +@Accessors(fluent = true) +public final class MetaOperationContext { + + private final CommandSender sender; + private final String type; + private final String target; + + private MetaOperationContext(@NotNull CommandSender sender, @NotNull String type, @NotNull String target) { + this.sender = sender; + this.type = type; + this.target = target; + } + + @NotNull + public Key typeToKey() { + return Key.of(type); + } + + @NotNull + public static MetaOperationContext of(@NotNull CommandSender sender, @NotNull String type, @NotNull String target) { + return new MetaOperationContext(sender, type, target); + } +} diff --git a/src/main/java/me/re4erka/lpmetaplus/operation/factory/MetaOperationFactory.java b/src/main/java/me/re4erka/lpmetaplus/operation/factory/MetaOperationFactory.java new file mode 100644 index 0000000..461ad1e --- /dev/null +++ b/src/main/java/me/re4erka/lpmetaplus/operation/factory/MetaOperationFactory.java @@ -0,0 +1,30 @@ +package me.re4erka.lpmetaplus.operation.factory; + +import me.re4erka.lpmetaplus.LPMetaPlus; +import me.re4erka.lpmetaplus.configuration.type.CustomMeta; +import me.re4erka.lpmetaplus.operation.type.EditableMetaOperation; +import me.re4erka.lpmetaplus.operation.type.ReturnedMetaOperation; +import me.re4erka.lpmetaplus.session.MetaSession; +import me.re4erka.lpmetaplus.util.Key; +import org.jetbrains.annotations.NotNull; + +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +public final class MetaOperationFactory { + + private MetaOperationFactory() { + throw new UnsupportedOperationException("This is a factory class and cannot be instantiated"); + } + + public static ReturnedMetaOperation createReturned(@NotNull LPMetaPlus lpMetaPlus, + @NotNull BiConsumer thenAction) { + return new ReturnedMetaOperation(lpMetaPlus, thenAction); + } + + public static EditableMetaOperation createEditable(@NotNull LPMetaPlus lpMetaPlus, + @NotNull BiConsumer editable, + @NotNull Consumer thenAction) { + return new EditableMetaOperation(lpMetaPlus, editable, thenAction); + } +} diff --git a/src/main/java/me/re4erka/lpmetaplus/operation/type/EditableMetaOperation.java b/src/main/java/me/re4erka/lpmetaplus/operation/type/EditableMetaOperation.java new file mode 100644 index 0000000..67b8ef9 --- /dev/null +++ b/src/main/java/me/re4erka/lpmetaplus/operation/type/EditableMetaOperation.java @@ -0,0 +1,35 @@ +package me.re4erka.lpmetaplus.operation.type; + +import me.re4erka.lpmetaplus.LPMetaPlus; +import me.re4erka.lpmetaplus.configuration.type.CustomMeta; +import me.re4erka.lpmetaplus.operation.AbstractMetaOperation; +import me.re4erka.lpmetaplus.operation.context.MetaOperationContext; +import me.re4erka.lpmetaplus.session.MetaSession; +import me.re4erka.lpmetaplus.util.Key; +import org.jetbrains.annotations.NotNull; + +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +public class EditableMetaOperation extends AbstractMetaOperation { + + private final BiConsumer editable; + private final Consumer thenAction; + + public EditableMetaOperation(@NotNull LPMetaPlus lpMetaPlus, + @NotNull BiConsumer editable, + @NotNull Consumer thenAction) { + super(lpMetaPlus); + this.editable = editable; + this.thenAction = thenAction; + } + + @Override + public void execute(@NotNull MetaOperationContext context, boolean silent) { + ifMetaPresent(context, meta -> lpMetaPlus.getMetaManager().findUser(context.target()) + .thenAccept(session -> tryRunOperation(session, () -> session.edit( + editor -> editable.accept(context.typeToKey(), editor), silent))) + .thenRun(() -> runIfNotSilent(() -> thenAction.accept(meta), silent)) + .exceptionally(throwable -> throwException(context, throwable))); + } +} diff --git a/src/main/java/me/re4erka/lpmetaplus/operation/type/ReturnedMetaOperation.java b/src/main/java/me/re4erka/lpmetaplus/operation/type/ReturnedMetaOperation.java new file mode 100644 index 0000000..0a9bc0a --- /dev/null +++ b/src/main/java/me/re4erka/lpmetaplus/operation/type/ReturnedMetaOperation.java @@ -0,0 +1,28 @@ +package me.re4erka.lpmetaplus.operation.type; + +import me.re4erka.lpmetaplus.LPMetaPlus; +import me.re4erka.lpmetaplus.configuration.type.CustomMeta; +import me.re4erka.lpmetaplus.operation.AbstractMetaOperation; +import me.re4erka.lpmetaplus.operation.context.MetaOperationContext; +import org.jetbrains.annotations.NotNull; + +import java.util.function.BiConsumer; + +public class ReturnedMetaOperation extends AbstractMetaOperation { + + private final BiConsumer thenAction; + + public ReturnedMetaOperation(@NotNull LPMetaPlus lpMetaPlus, + @NotNull BiConsumer thenAction) { + super(lpMetaPlus); + this.thenAction = thenAction; + } + + @Override + public void execute(@NotNull MetaOperationContext context, boolean silent) { + ifMetaPresent(context, meta -> lpMetaPlus.getMetaManager().findUser(context.target()) + .thenApply(session -> tryRunOperationAndReturn(session, () -> session.get(context.typeToKey()))) + .thenAccept(count -> runIfNotSilent(() -> thenAction.accept(meta, count), silent)) + .exceptionally(throwable -> throwException(context, throwable))); + } +} diff --git a/src/main/java/me/re4erka/lpmetaplus/placeholder/MetaPlaceholder.java b/src/main/java/me/re4erka/lpmetaplus/placeholder/MetaPlaceholder.java new file mode 100644 index 0000000..15f268a --- /dev/null +++ b/src/main/java/me/re4erka/lpmetaplus/placeholder/MetaPlaceholder.java @@ -0,0 +1,104 @@ +package me.re4erka.lpmetaplus.placeholder; + +import me.clip.placeholderapi.expansion.PlaceholderExpansion; +import me.re4erka.lpmetaplus.LPMetaPlus; +import me.re4erka.lpmetaplus.configuration.type.CustomMeta; +import me.re4erka.lpmetaplus.manager.type.MetaManager; +import me.re4erka.lpmetaplus.session.MetaSession; +import me.re4erka.lpmetaplus.util.Formatter; +import me.re4erka.lpmetaplus.util.Key; +import org.apache.commons.lang.StringUtils; +import org.bukkit.OfflinePlayer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Locale; + +public class MetaPlaceholder extends PlaceholderExpansion { + + private final LPMetaPlus lpMetaPlus; + + private final MetaManager metaManager; + + private static final String SEPARATOR = "_"; + + private static final String META_NOT_FOUND = "meta not found"; + private static final String PLAYER_NOT_ONLINE = "player not online"; + private static final String PLACEHOLDER_NOT_FOUND = "placeholder not found"; + + private static final String EMPTY_DISPLAY_NAME = "empty display name"; + private static final String EMPTY_SYMBOL = "empty symbol"; + + public MetaPlaceholder(@NotNull LPMetaPlus lpMetaPlus) { + this.lpMetaPlus = lpMetaPlus; + + this.metaManager = lpMetaPlus.getMetaManager(); + } + + @NotNull + @Override + public String getIdentifier() { + return "meta"; + } + + @NotNull + @Override + public String getAuthor() { + return "RE4ERKA"; + } + + @NotNull + @Override + public String getVersion() { + return lpMetaPlus.getDescription().getVersion(); + } + + @Override + @Nullable + public String onRequest(@NotNull OfflinePlayer player, @NotNull String rawParams) { + if (!player.isOnline()) { + return PLAYER_NOT_ONLINE; + } + + final String[] params = convert(rawParams); + final Key key = Key.of(params[0]); + + if (!lpMetaPlus.metas().contains(key)) { + return META_NOT_FOUND; + } + + if (params.length == 1) { + try (MetaSession session = metaManager.getUser(player)) { + return session.getAsString(key); + } + } + + final CustomMeta meta = lpMetaPlus.metas().type(key); + switch (params.length) { + case 2: { + if (params[1].equals("symbol")) { + return meta.symbol() == null ? EMPTY_SYMBOL : Character.toString(meta.symbol()); + } + } + case 3: { + if (params[1].equals("with") && params[2].equals("symbol")) { + try (MetaSession session = metaManager.getUser(player)) { + return session.getAsString(key) + meta.symbol(); + } + } else if (params[1].equals("display") && params[2].equals("name")) { + return meta.displayName() == null ? EMPTY_DISPLAY_NAME : Formatter.format(meta.displayName()); + } else if (params[1].equals("default") && params[2].equals("value")) { + return meta.defaultValueToString(); + } + } + } + + return PLACEHOLDER_NOT_FOUND; + } + + @NotNull + private String[] convert(@NotNull String params) { + params = params.toLowerCase(Locale.ROOT); + return StringUtils.split(params, SEPARATOR, 3); + } +} diff --git a/src/main/java/me/re4erka/lpmetaplus/plugin/BasePlugin.java b/src/main/java/me/re4erka/lpmetaplus/plugin/BasePlugin.java new file mode 100644 index 0000000..7b3659c --- /dev/null +++ b/src/main/java/me/re4erka/lpmetaplus/plugin/BasePlugin.java @@ -0,0 +1,76 @@ +package me.re4erka.lpmetaplus.plugin; + +import me.re4erka.lpmetaplus.LPMetaPlus; +import me.re4erka.lpmetaplus.api.LPMetaPlusAPI; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.NotNull; + +import java.util.function.Consumer; +import java.util.logging.Level; + +public abstract class BasePlugin

extends JavaPlugin { + + @Override + public void onEnable() { + throwIfNotRelocated(); + loadConfigurations(); + enable(); + registerCommands(); + LPMetaPlusAPI.register(self()); + } + + @Override + public void onDisable() { + LPMetaPlusAPI.unregister(); + } + + public void reload() { + loadConfigurations(); + } + + public abstract void enable(); + + public void logInfo(@NotNull String message) { + getLogger().log(Level.INFO, message); + } + + public void logError(@NotNull String message) { + getLogger().log(Level.SEVERE, message); + } + + public void logError(@NotNull String message, @NotNull Throwable throwable) { + getLogger().log(Level.SEVERE, message, throwable); + } + + protected abstract void loadConfigurations(); + protected abstract void registerCommands(); + + protected abstract P self(); + + protected void initialize(@NotNull String name, @NotNull Consumer

initialize) { + getLogger().log(Level.INFO, "Initializing {0}...", name); + try { + initialize.accept(self()); + } catch (Exception exception) { + logError("An error occurred while initializing " + name, exception); + } + } + + protected boolean isSupportPlaceholderAPI() { + return getServer().getPluginManager().getPlugin("PlaceholderAPI") != null; + } + + protected void logNotFoundPlaceholderAPI() { + logInfo("PlaceholderAPI was not found. MetaPlaceholder class is ignored."); + } + + private void throwIfNotRelocated() { + try { + Class.forName("me.re4erka.lpmetaplus.libraries.command.bukkit.BukkitCommandManager"); + Class.forName("me.re4erka.lpmetaplus.libraries.configuration.YamlConfigurations"); + } catch (ClassNotFoundException exception) { + logError("Please relocate dependencies correctly or buy the plugin because it won't have this error :)"); + throw new RuntimeException(exception); + } + } +} diff --git a/src/main/java/me/re4erka/lpmetaplus/session/MetaSession.java b/src/main/java/me/re4erka/lpmetaplus/session/MetaSession.java new file mode 100644 index 0000000..86b4ba6 --- /dev/null +++ b/src/main/java/me/re4erka/lpmetaplus/session/MetaSession.java @@ -0,0 +1,223 @@ +package me.re4erka.lpmetaplus.session; + +import lombok.Getter; +import me.re4erka.lpmetaplus.LPMetaPlus; +import me.re4erka.lpmetaplus.action.MetaAction; +import me.re4erka.lpmetaplus.util.Key; +import net.luckperms.api.actionlog.Action; +import net.luckperms.api.actionlog.ActionLogger; +import net.luckperms.api.model.data.DataType; +import net.luckperms.api.model.data.NodeMap; +import net.luckperms.api.model.group.Group; +import net.luckperms.api.model.user.User; +import net.luckperms.api.model.user.UserManager; +import net.luckperms.api.node.Node; +import net.luckperms.api.node.NodeType; +import net.luckperms.api.node.types.MetaNode; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Range; + +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; + +public final class MetaSession implements AutoCloseable { + + private final LPMetaPlus lpMetaPlus; + + private final UserManager userManager; + private final ActionLogger actionLogger; + + private final Group defaultGroup; + + private final User user; + @Getter + private final boolean lookup; + + private static final UUID CONSOLE_UUID = new UUID(0, 0); + + public MetaSession(@NotNull LPMetaPlus lpMetaPlus, + @NotNull UserManager userManager, + @NotNull ActionLogger actionLogger, + @NotNull Group defaultGroup, + @NotNull User user, boolean lookup) { + this.lpMetaPlus = lpMetaPlus; + this.userManager = userManager; + this.actionLogger = actionLogger; + + this.defaultGroup = defaultGroup; + + this.user = user; + this.lookup = lookup; + } + + public int get(@NotNull Key key) { + return zeroOrGreaterParse( + getAsString(key) + ); + } + + @NotNull + public String getAsString(@NotNull Key key) { + return getFirst(key) + .getMetaValue(); + } + + public void edit(@NotNull Consumer edit, boolean silent) { + final Editor editor = new Editor(silent); + edit.accept(editor); + saveUser().thenRun(() -> logAction(editor)); + } + + @Override + public void close() { + if (lookup) { + userManager.cleanupUser(user); + } + } + + @NotNull + private MetaNode getFirst(@NotNull Key key) { + return metas().stream() + .filter(node -> isEquals(node, key)) + .findFirst() + .orElse(getFirstFromDefaultGroup(key)); + } + + @NotNull + @SuppressWarnings("ConstantConditions") + private MetaNode getFirstFromDefaultGroup(@NotNull Key key) { + return defaultGroup.getNodes(NodeType.META).stream() + .filter(node -> isEquals(node, key)) + .findFirst() + .orElseThrow(() -> throwMetaNotFound(key)); + } + + @NotNull + private Collection metas() { + return user.getNodes(NodeType.META); + } + + private boolean isEquals(@NotNull MetaNode node, @NotNull Key key) { + return node.getMetaKey().equals(key.toLowerCase()); + } + + private void logAction(@NotNull Editor editor) { + final Set actions = editor.actions; + + if (editor.silent) { + actions.forEach(action -> actionLogger.submitToStorage(buildAction(action))); + } else { + actions.forEach(action -> actionLogger.submit(buildAction(action))); + } + } + + @NotNull + private Action buildAction(@NotNull MetaAction action) { + return actionLogger.actionBuilder() + .timestamp(action.timestamp()) + .source(CONSOLE_UUID) + .sourceName("LPMetaPlus") + .target(user.getUniqueId()) + .targetName(user.getFriendlyName()) + .targetType(Action.Target.Type.USER) + .description(action.toDescription()) + .build(); + } + + @NotNull + private CompletableFuture saveUser() { + return userManager.saveUser(user); + } + + private int zeroOrGreaterParse(@NotNull String value) { + final int i = Integer.parseInt(value); + return Math.max(i, 0); + } + + private RuntimeException throwMetaNotFound(@NotNull Key key) { + lpMetaPlus.logError("When trying to find meta for a user, it was not found. " + + "This meta may have been unset/cleared from the 'default' group, " + + "please restart the plugin or return the meta: '" + key + "'"); + return new IllegalStateException("MetaNode cannot be null!"); + } + + public final class Editor { + + private final Set actions; + private final boolean silent; + + private Editor(boolean silent) { + this.actions = new HashSet<>(1); + this.silent = silent; + } + + public void set(@NotNull Key key, @Range(from = 0, to = Integer.MAX_VALUE) int count) { + remove(key); + addNode(buildMetaNode(key, zeroOrGreaterToString(count))); + addAction(buildMetaAction(MetaAction.Type.SET, key, count)); + } + + public void give(@NotNull Key key, @Range(from = 0, to = Integer.MAX_VALUE) int count) { + removeThen(key, previousCount -> { + addNode(buildMetaNode(key, zeroOrGreaterToString(previousCount + count))); + addAction(buildMetaAction(MetaAction.Type.GIVE, key, count)); + }); + } + + public void take(@NotNull Key key, @Range(from = 0, to = Integer.MAX_VALUE) int count) { + removeThen(key, previousCount -> { + addNode(buildMetaNode(key, zeroOrGreaterToString(previousCount - count))); + addAction(buildMetaAction(MetaAction.Type.TAKE, key, count)); + }); + } + + public int remove(@NotNull Key key) { + final MetaNode node = getFirst(key); + final String value = node.getMetaValue(); + removeNode(node); + + return Integer.parseInt(value); + } + + public void removeThen(@NotNull Key key, @NotNull Consumer then) { + then.accept(remove(key)); + } + + @NotNull + private MetaNode buildMetaNode(@NotNull Key key, @NotNull String value) { + return MetaNode.builder() + .key(key.toLowerCase()).value(value) + .build(); + } + + private void addNode(@NotNull Node node) { + nodeMap().add(node); + } + + private void removeNode(@NotNull Node node) { + nodeMap().remove(node); + } + + @NotNull + private NodeMap nodeMap() { + return user.getData(DataType.NORMAL); + } + + @NotNull + private MetaAction buildMetaAction(@NotNull MetaAction.Type type, @NotNull Key key, int count) { + return MetaAction.builder() + .type(type).key(key).count(count) + .build(); + } + + private void addAction(@NotNull MetaAction action) { + actions.add(action); + } + + @NotNull + private String zeroOrGreaterToString(int i) { + return i < 0 ? "0" : Integer.toString(i); + } + } +} diff --git a/src/main/java/me/re4erka/lpmetaplus/util/Formatter.java b/src/main/java/me/re4erka/lpmetaplus/util/Formatter.java new file mode 100644 index 0000000..cc5c07b --- /dev/null +++ b/src/main/java/me/re4erka/lpmetaplus/util/Formatter.java @@ -0,0 +1,75 @@ +package me.re4erka.lpmetaplus.util; + +import lombok.experimental.UtilityClass; +import org.bukkit.ChatColor; +import org.jetbrains.annotations.NotNull; + +@UtilityClass +public final class Formatter { + + @NotNull + public String format(@NotNull String input) { + if (input.isEmpty()) { + return ""; + } + + final StringBuilder result = new StringBuilder(input.length()); + final char[] chars = input.toCharArray(); + + for (int i = 0; i < chars.length; i++) { + + final char current = chars[i]; + if (current == '&' && i + 1 < chars.length) { + + final char next = chars[i + 1]; + if (isColorCode(next)) { + result.append('§').append(next); + i++; + } else { + result.append(current); + } + } else if (current == '#' && i + 6 < chars.length && isHexColor(chars, i + 1)) { + result.append("§x"); + for (int j = 0; j < 6; j++) { + result.append('§').append(chars[i + 1 + j]); + } + + i += 6; + } else { + result.append(current); + } + } + + return result.toString(); + } + + @NotNull + public String strip(@NotNull String input) { + return ChatColor.stripColor( + format(input) + ); + } + + private boolean isColorCode(char c) { + return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F') + || c == 'k' || c == 'l' || c == 'm' || c == 'n' || c == 'o' || c == 'r'; + } + + private boolean isHexColor(char[] chars, int start) { + if (start + 5 >= chars.length) { + return false; + } + + for (int i = start; i < start + 6; i++) { + if (!isHexChar(chars[i])) { + return false; + } + } + + return true; + } + + private boolean isHexChar(char c) { + return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); + } +} diff --git a/src/main/java/me/re4erka/lpmetaplus/util/Key.java b/src/main/java/me/re4erka/lpmetaplus/util/Key.java new file mode 100644 index 0000000..9e3ac55 --- /dev/null +++ b/src/main/java/me/re4erka/lpmetaplus/util/Key.java @@ -0,0 +1,204 @@ +package me.re4erka.lpmetaplus.util; + +import org.jetbrains.annotations.NotNull; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +public class Key implements CharSequence, Comparable { + + private final byte[] characters; + private final int hash; // Кэшируем хэш-код для производительности + + public Key(@NotNull String input) { + throwIfNotAscii(input); + this.characters = toUpperCaseAscii(input); + this.hash = computeHashCode(); + } + + private Key(byte[] characters) { + this.characters = characters; + this.hash = computeHashCode(); + } + + @NotNull + public Key append(@NotNull String suffix) { + return createWithNewString(suffix, false); + } + + @NotNull + public Key prepend(@NotNull String prefix) { + return createWithNewString(prefix, true); + } + + @NotNull + private Key createWithNewString(@NotNull String additional, boolean prepend) { + throwIfNotAscii(additional); + + final byte[] additionalBytes = toUpperCaseAscii(additional); + final byte[] newCharacters = new byte[this.characters.length + additionalBytes.length]; + + if (prepend) { + System.arraycopy(additionalBytes, 0, newCharacters, 0, additionalBytes.length); + System.arraycopy(this.characters, 0, newCharacters, additionalBytes.length, this.characters.length); + } else { + System.arraycopy(this.characters, 0, newCharacters, 0, this.characters.length); + System.arraycopy(additionalBytes, 0, newCharacters, this.characters.length, additionalBytes.length); + } + + return new Key(newCharacters); + } + + private boolean isAscii(@NotNull String input) { + for (char c : input.toCharArray()) { + if (c > 127) { + return false; + } + } + + return true; + } + + private byte[] toUpperCaseAscii(@NotNull String input) { + final byte[] bytes = input.getBytes(StandardCharsets.US_ASCII); + + for (int i = 0; i < bytes.length; i++) { + if (bytes[i] >= 'a' && bytes[i] <= 'z') { + bytes[i] = (byte) (bytes[i] - ('a' - 'A')); + } + } + + return bytes; + } + + private void throwIfNotAscii(@NotNull String input) { + if (!isAscii(input)) { + throw new IllegalArgumentException("Input contains non-ASCII characters"); + } + } + + @Override + public int length() { + return characters.length; + } + + @Override + public char charAt(int index) { + if (index < 0 || index >= characters.length) { + throw new IndexOutOfBoundsException("Index: " + index + ", Length: " + characters.length); + } + + return (char) characters[index]; + } + + @NotNull + @Override + public CharSequence subSequence(int start, int end) { + if (start < 0 || end > characters.length || start > end) { + throw new IndexOutOfBoundsException("Start: " + start + ", End: " + end + ", Length: " + characters.length); + } + + return new String(characters, start, end - start, StandardCharsets.US_ASCII); + } + + @NotNull + @Override + public String toString() { + return new String(characters, StandardCharsets.US_ASCII); + } + + @NotNull + public String toLowerCase() { + final byte[] lowerCaseBytes = new byte[characters.length]; + + for (int i = 0; i < characters.length; i++) { + final byte currentByte = characters[i]; + + if (currentByte >= 'A' && currentByte <= 'Z') { + lowerCaseBytes[i] = (byte) (currentByte + ('a' - 'A')); + } else { + lowerCaseBytes[i] = currentByte; + } + } + + return new String(lowerCaseBytes, StandardCharsets.US_ASCII); + } + + @Override + public int compareTo(final Key other) { + final int len1 = this.characters.length; + final int len2 = other.characters.length; + + final int lim = Math.min(len1, len2); + + for (int i = 0; i < lim; i++) { + final int b1 = this.characters[i] & 0xFF; + final int b2 = other.characters[i] & 0xFF; + + if (b1 != b2) { + return b1 - b2; + } + } + + return len1 - len2; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + final Key other = (Key) obj; + return Arrays.equals(this.characters, other.characters); + } + + public boolean equals(CharSequence charSequence) { + if (charSequence == null) { + return false; + } + + if (charSequence.length() != characters.length) { + return false; + } + + if (charSequence instanceof String) { + return charSequence.equals( + toString() + ); + } + + final int length = charSequence.length(); + for (int i = 0; i < length; i++) { + if ((char) characters[i] != charSequence.charAt(i)) { + return false; + } + } + + return true; + } + + @Override + public int hashCode() { + return hash; + } + + private int computeHashCode() { + int result = 1; + + for (byte b : characters) { + result = 31 * result + (b & 0xFF); + } + + return result; + } + + @NotNull + public static Key of(final @NotNull String input) { + return new Key(input); + } +} diff --git a/src/main/java/me/re4erka/lpmetaplus/util/OfflineUUID.java b/src/main/java/me/re4erka/lpmetaplus/util/OfflineUUID.java new file mode 100644 index 0000000..4116206 --- /dev/null +++ b/src/main/java/me/re4erka/lpmetaplus/util/OfflineUUID.java @@ -0,0 +1,19 @@ +package me.re4erka.lpmetaplus.util; + +import lombok.experimental.UtilityClass; +import org.jetbrains.annotations.NotNull; + +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +@UtilityClass +public final class OfflineUUID { + + private static final String PREFIX = "OfflinePlayer:"; + + @NotNull + public UUID fromName(@NotNull String name) { + final String prefixedName = PREFIX + name; + return UUID.nameUUIDFromBytes(prefixedName.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/src/main/java/me/re4erka/lpmetaplus/util/SortedMaps.java b/src/main/java/me/re4erka/lpmetaplus/util/SortedMaps.java new file mode 100644 index 0000000..3d4c03e --- /dev/null +++ b/src/main/java/me/re4erka/lpmetaplus/util/SortedMaps.java @@ -0,0 +1,134 @@ +package me.re4erka.lpmetaplus.util; + +import lombok.experimental.UtilityClass; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Unmodifiable; + +import java.util.Collections; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; + +@UtilityClass +public final class SortedMaps { + + @NotNull + @Unmodifiable + public SortedMap of(@NotNull K key1, @NotNull V value1) { + return unmodifiableSortedMap(createMap(key1, value1)); + } + + @NotNull + @Unmodifiable + public SortedMap of(@NotNull K key1, @NotNull V value1, @NotNull K key2, @NotNull V value2) { + return unmodifiableSortedMap(createMap(key1, value1, key2, value2)); + } + + @NotNull + @Unmodifiable + public SortedMap of(@NotNull K key1, @NotNull V value1, @NotNull K key2, @NotNull V value2, + @NotNull K key3, @NotNull V value3) { + return unmodifiableSortedMap(createMap(key1, value1, key2, value2, key3, value3)); + } + + @NotNull + @Unmodifiable + public SortedMap of(@NotNull K key1, @NotNull V value1, @NotNull K key2, @NotNull V value2, + @NotNull K key3, @NotNull V value3, @NotNull K key4, @NotNull V value4) { + return unmodifiableSortedMap(createMap(key1, value1, key2, value2, key3, value3, key4, value4)); + } + + @NotNull + @Unmodifiable + public SortedMap of(@NotNull K key1, @NotNull V value1, @NotNull K key2, @NotNull V value2, + @NotNull K key3, @NotNull V value3, @NotNull K key4, @NotNull V value4, + @NotNull K key5, @NotNull V value5) { + return unmodifiableSortedMap(createMap(key1, value1, key2, value2, key3, value3, key4, value4, key5, value5)); + } + + @NotNull + @Unmodifiable + public SortedMap of(@NotNull K key1, @NotNull V value1, @NotNull K key2, @NotNull V value2, + @NotNull K key3, @NotNull V value3, @NotNull K key4, @NotNull V value4, + @NotNull K key5, @NotNull V value5, @NotNull K key6, @NotNull V value6) { + return unmodifiableSortedMap( + createMap(key1, value1, key2, value2, key3, value3, key4, value4, key5, value5, key6, value6) + ); + } + + @NotNull + @Unmodifiable + public SortedMap of(@NotNull K key1, @NotNull V value1, @NotNull K key2, @NotNull V value2, + @NotNull K key3, @NotNull V value3, @NotNull K key4, @NotNull V value4, + @NotNull K key5, @NotNull V value5, @NotNull K key6, @NotNull V value6, + @NotNull K key7, @NotNull V value7) { + return unmodifiableSortedMap( + createMap(key1, value1, key2, value2, key3, value3, key4, value4, key5, value5, key6, value6, key7, value7) + ); + } + + @NotNull + @Unmodifiable + public SortedMap of(@NotNull K key1, @NotNull V value1, @NotNull K key2, @NotNull V value2, + @NotNull K key3, @NotNull V value3, @NotNull K key4, @NotNull V value4, + @NotNull K key5, @NotNull V value5, @NotNull K key6, @NotNull V value6, + @NotNull K key7, @NotNull V value7, @NotNull K key8, @NotNull V value8) { + return unmodifiableSortedMap( + createMap(key1, value1, key2, value2, key3, value3, key4, value4, key5, value5, key6, + value6, key7, value7, key8, value8) + ); + } + + @NotNull + @Unmodifiable + public SortedMap of(@NotNull K key1, @NotNull V value1, @NotNull K key2, @NotNull V value2, + @NotNull K key3, @NotNull V value3, @NotNull K key4, @NotNull V value4, + @NotNull K key5, @NotNull V value5, @NotNull K key6, @NotNull V value6, + @NotNull K key7, @NotNull V value7, @NotNull K key8, @NotNull V value8, + @NotNull K key9, @NotNull V value9) { + return unmodifiableSortedMap( + createMap(key1, value1, key2, value2, key3, value3, key4, value4, key5, value5, key6, + value6, key7, value7, key8, value8, key9, value9) + ); + } + + @NotNull + @SafeVarargs + @Unmodifiable + public SortedMap ofEntries(@NotNull Map.Entry... entries) { + @SuppressWarnings("SortedCollectionWithNonComparableKeys") + final TreeMap map = new TreeMap<>(); + for (Map.Entry entry : entries) { + map.put(entry.getKey(), entry.getValue()); + } + + return unmodifiableSortedMap(map); + } + + @NotNull + @Unmodifiable + private SortedMap unmodifiableSortedMap(@NotNull TreeMap map) { + return Collections.unmodifiableSortedMap(map); + } + + @NotNull + @Unmodifiable + private TreeMap createMap(@NotNull Object... keyValues) { + if (keyValues.length % 2 != 0) { + throw new IllegalArgumentException("Odd number of arguments passed. Key-value pairs expected."); + } + + @SuppressWarnings("SortedCollectionWithNonComparableKeys") + final TreeMap map = new TreeMap<>(); + for (int i = 0; i < keyValues.length; i += 2) { + @SuppressWarnings("unchecked") + final K key = (K) keyValues[i]; + @SuppressWarnings("unchecked") + final V value = (V) keyValues[i + 1]; + + map.put(key, value); + } + + return map; + } +} diff --git a/src/main/java/me/re4erka/lpmetaplus/util/UnsignedInts.java b/src/main/java/me/re4erka/lpmetaplus/util/UnsignedInts.java new file mode 100644 index 0000000..3a975d3 --- /dev/null +++ b/src/main/java/me/re4erka/lpmetaplus/util/UnsignedInts.java @@ -0,0 +1,15 @@ +package me.re4erka.lpmetaplus.util; + +import lombok.experimental.UtilityClass; + +@UtilityClass +public final class UnsignedInts { + + public boolean is(int amount) { + return amount > 0; + } + + public boolean isNot(int amount) { + return !is(amount); + } +}