diff --git a/build.gradle b/build.gradle index 82ca2f5fe..4d3d4c762 100644 --- a/build.gradle +++ b/build.gradle @@ -5,12 +5,13 @@ plugins { id 'io.freefair.lombok' version libs.versions.delombok } +final def TEST_MODULE = 'testing' +final def DEMO_MODULE = 'demo' +final def VERSION_VARIABLE = 'MINECRAFT_VERSION' + group = 'it.angrybear' version = '4.0' -final def TEST_MODULE = "testing" -final def VERSION_VARIABLE = "MINECRAFT_VERSION" - allprojects { apply plugin: 'java-library' apply plugin: 'maven-publish' @@ -26,7 +27,7 @@ allprojects { def obsolete_version = libs.versions.spigot.obsolete.get() this.ext.getParentFromProject = { project -> - return new HashMap<>(project.getProperties()).get("parent") + return new HashMap<>(project.getProperties()).get('parent') } this.ext.getProjectGroupId = { @@ -37,7 +38,7 @@ allprojects { tmp = getParentFromProject(tmp) } if (groupId.size() > 0) - groupId = "." + groupId.substring(0, groupId.length() - 1) + groupId = ".${groupId.substring(0, groupId.length() - 1)}" return "${rootProject.group}${groupId}" } @@ -50,18 +51,18 @@ allprojects { repositories { mavenCentral() maven { - name = "spigotmc-repo" + name = 'spigotmc-repo' url = 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' } maven { url = 'https://oss.sonatype.org/content/repositories/snapshots' } maven { url = 'https://oss.sonatype.org/content/repositories/central' } maven { - name = "JitPack" - url = "https://jitpack.io" + name = 'JitPack' + url = 'https://jitpack.io' } maven { - name = "Fulminazzo repository" - url = "https://repo.fulminazzo.it/releases" + name = 'Fulminazzo repository' + url = 'https://repo.fulminazzo.it/releases' } } @@ -75,49 +76,50 @@ allprojects { def projectName = project.name - if (projectName.contains("serializer")) api(libs.yamlparser) + if (projectName.contains('serializer')) api(libs.yamlparser) - if (projectName.contains("bukkit")) { + if (projectName.contains('bukkit')) { compileOnly libs.spigot testCompileOnly libs.spigot.latest testRuntimeOnly "org.spigotmc:spigot-api:${latest_version}" - String numVersion = latest_version.substring(2, latest_version.indexOf(".", 2)) - def index = numVersion.indexOf("-") + String numVersion = latest_version.substring(2, latest_version.indexOf('.', 2)) + def index = numVersion.indexOf('-') if (index != -1) numVersion = numVersion.substring(0, index) if (Double.valueOf(numVersion) >= 13) testImplementation libs.jbukkit else testImplementation libs.jbukkit.legacy } - if (projectName.contains("legacy")) { + if (projectName.contains('legacy')) { testCompileOnly libs.spigot.legacy testRuntimeOnly "org.spigotmc:spigot-api:${legacy_version}" testImplementation libs.jbukkit.legacy } - if (projectName.contains("obsolete")) { + if (projectName.contains('obsolete')) { testCompileOnly libs.spigot.obsolete testRuntimeOnly "org.spigotmc:spigot-api:${obsolete_version}" testImplementation libs.jbukkit.legacy } - if (projectName.contains("-")) { - def name = projectName.substring(0, projectName.indexOf("-")) - def id = projectName.substring(projectName.indexOf("-") + 1) - if (!projectName.contains("base")) api(project(":${name}:${name}-base")) - if (name != "common" && id != "legacy" && id != "obsolete") + if (projectName.contains('-')) { + def name = projectName.substring(0, projectName.indexOf('-')) + def id = projectName.substring(projectName.indexOf('-') + 1) + if (!projectName.contains('base')) api(project(":${name}:${name}-base")) + if (name != 'common' && id != 'legacy' && id != 'obsolete') api(project(":common:common-${id}")) } - if (!projectName.contains("-")) - subprojects.findAll { (it.name != TEST_MODULE) } .each {api project(it.path)} - testCompileOnly libs.lombok testAnnotationProcessor libs.lombok - if (!project.name.equals(TEST_MODULE)) testImplementation project(":" + TEST_MODULE) + if (!project.name.equals(TEST_MODULE)) testImplementation project(":${TEST_MODULE}") + + if (project.name.equals(DEMO_MODULE)) + rootProject.subprojects.findAll { it.name != TEST_MODULE && it.name != DEMO_MODULE } + .each { implementation project(it.path) } } if (project.name.equals(TEST_MODULE)) return @@ -126,9 +128,9 @@ allprojects { def projectName = project.name if (System.getenv(VERSION_VARIABLE) == null) { - if (projectName.contains("bukkit")) environment VERSION_VARIABLE, latest_version - else if (projectName.contains("legacy")) environment VERSION_VARIABLE, legacy_version - else if (projectName.contains("obsolete")) environment VERSION_VARIABLE, obsolete_version + if (projectName.contains('bukkit')) environment VERSION_VARIABLE, latest_version + else if (projectName.contains('legacy')) environment VERSION_VARIABLE, legacy_version + else if (projectName.contains('obsolete')) environment VERSION_VARIABLE, obsolete_version } def env = System.getenv(VERSION_VARIABLE) @@ -136,14 +138,14 @@ allprojects { useJUnitPlatform() } - tasks.register("testBukkit") { + tasks.register('testBukkit') { final def minecraftVersion = getMinecraftVersion() println "Using Minecraft version ${minecraftVersion}" latest_version = minecraftVersion legacy_version = minecraftVersion obsolete_version = minecraftVersion - subprojects.findAll { it.path.contains("bukkit") }.each {dependsOn "${it.path}:test" } + subprojects.findAll { it.path.contains('bukkit') }.each {dependsOn "${it.path}:test" } } tasks.register('sourcesJar', Jar) { @@ -171,10 +173,10 @@ allprojects { repositories { maven { - url "https://repo.fulminazzo.it/releases" + url 'https://repo.fulminazzo.it/releases' credentials { - username = System.getenv("REPO_USERNAME") - password = System.getenv("REPO_PASSWORD") + username = System.getenv('REPO_USERNAME') + password = System.getenv('REPO_PASSWORD') } authentication { basic(BasicAuthentication) @@ -184,6 +186,11 @@ allprojects { } } +dependencies { + subprojects.findAll { it.name != TEST_MODULE && it.name != DEMO_MODULE } .each { api project(it.path) } + implementation project(":${DEMO_MODULE}") +} + testCodeCoverageReport { dependsOn test reports { diff --git a/common/base/src/main/java/it/angrybear/yagl/ClassEnum.java b/common/base/src/main/java/it/angrybear/yagl/ClassEnum.java index def3551d0..eed10be7f 100644 --- a/common/base/src/main/java/it/angrybear/yagl/ClassEnum.java +++ b/common/base/src/main/java/it/angrybear/yagl/ClassEnum.java @@ -129,7 +129,8 @@ private void checkValues() { if (!this.values.isEmpty()) return; for (Field field : this.clazz.getDeclaredFields()) if (field.getType().equals(this.clazz)) - this.values.put(field.getName().toUpperCase(), ReflectionUtils.get(field, this.clazz)); + ReflectionUtils.get(field, this.clazz).ifPresent(o -> + this.values.put(field.getName().toUpperCase(), (T) o)); } } } diff --git a/common/base/src/main/java/it/angrybear/yagl/utils/ObjectUtils.java b/common/base/src/main/java/it/angrybear/yagl/utils/ObjectUtils.java index bbef50b00..77546c283 100644 --- a/common/base/src/main/java/it/angrybear/yagl/utils/ObjectUtils.java +++ b/common/base/src/main/java/it/angrybear/yagl/utils/ObjectUtils.java @@ -6,7 +6,10 @@ import lombok.NoArgsConstructor; import org.jetbrains.annotations.NotNull; -import java.lang.reflect.*; +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.util.*; import java.util.stream.Collectors; @@ -63,37 +66,56 @@ public static O copy(final @NotNull T t, @NotNull Class claz clazz.getCanonicalName(), clazz.getCanonicalName())); } - Refl object = new Refl<>(clazz, new Object[0]); - for (final Field field : object.getNonStaticFields()) + final Refl object = new Refl<>(clazz, new Object[0]); + for (final Field field : object.getNonStaticFields()) { + field.setAccessible(true); try { - Object obj1 = ReflectionUtils.get(field, t); - if (obj1 instanceof Collection) { - Class tmpClass = obj1.getClass(); - // In the case of creation with Arrays.asList() - if (tmpClass.getCanonicalName().equals(Arrays.class.getCanonicalName() + ".ArrayList")) - tmpClass = ArrayList.class; - Class> finalClass = (Class>) tmpClass; - obj1 = ((Collection) obj1).stream() - .collect(Collectors.toCollection(() -> new Refl<>(finalClass, new Object[0]).getObject())); - } else if (obj1 instanceof Map) { - Map map = new HashMap<>(); - ((Map) obj1).putAll(map); - obj1 = map; - } else if (obj1 != null) - if (obj1.getClass().isArray()) { - Object[] tmp = (Object[]) obj1; - Object[] arr = (Object[]) Array.newInstance(obj1.getClass().getComponentType(), tmp.length); - System.arraycopy(tmp, 0, arr, 0, arr.length); - obj1 = arr; - } else - try { - Method copy = obj1.getClass().getDeclaredMethod("copy"); - obj1 = ReflectionUtils.setAccessible(copy).invoke(obj1); - } catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException ignored) {} - object.setFieldObject(field, obj1); - } catch (IllegalArgumentException ignored) {} - + ReflectionUtils.get(field, t).map(obj1 -> { + if (obj1 == null) return null; + else if (obj1 instanceof Collection) return copyCollection(obj1); + else if (obj1 instanceof Map) return copyMap(obj1); + else if (obj1.getClass().isArray()) return copyArray(obj1); + else return copyWithMethod(obj1); + }).ifPresent(obj1 -> object.setFieldObject(field, obj1)); + } catch (IllegalArgumentException e) { + if (!e.getMessage().contains("Can not set")) throw e; + } + } return object.getObject(); } + private static @NotNull Object[] copyArray(final @NotNull Object obj1) { + Object[] tmp = (Object[]) obj1; + Object[] arr = (Object[]) Array.newInstance(obj1.getClass().getComponentType(), tmp.length); + System.arraycopy(tmp, 0, arr, 0, arr.length); + return arr; + } + + private static @NotNull Map copyMap(final @NotNull Object obj1) { + Map map = new HashMap<>(); + ((Map) obj1).putAll(map); + return map; + } + + private static @NotNull Collection copyCollection(final @NotNull Object obj1) { + Class tmpClass = obj1.getClass(); + // In the case of creation with Arrays.asList() + if (tmpClass.getCanonicalName().equals(Arrays.class.getCanonicalName() + ".ArrayList")) + tmpClass = ArrayList.class; + Class> finalClass = (Class>) tmpClass; + return ((Collection) obj1).stream() + .collect(Collectors.toCollection(() -> new Refl<>(finalClass, new Object[0]).getObject())); + } + + private static @NotNull Object copyWithMethod(final @NotNull Object obj1) { + try { + Method copy = obj1.getClass().getDeclaredMethod("copy"); + return ReflectionUtils.setAccessible(copy) + .map(m -> m.invoke(obj1)) + .orElseGet(obj1); + } catch (NoSuchMethodException e) { + return obj1; + } + } + } diff --git a/common/base/src/test/java/it/angrybear/yagl/utils/ObjectUtilsTest.java b/common/base/src/test/java/it/angrybear/yagl/utils/ObjectUtilsTest.java index 8226c0271..95f0ddcbb 100644 --- a/common/base/src/test/java/it/angrybear/yagl/utils/ObjectUtilsTest.java +++ b/common/base/src/test/java/it/angrybear/yagl/utils/ObjectUtilsTest.java @@ -38,12 +38,23 @@ void testCopyOfData() { assertNotEquals(c1.copiable, c2.copiable); } + @Test + void testCopyThrowsIllegalArgument() { + Throwable exception = assertThrowsExactly(IllegalArgumentException.class, () -> + ObjectUtils.copy(new GeneralCopyException())); + assertEquals("Everything good", exception.getMessage()); + } + private static class GeneralCopy { List list = Arrays.asList("hello", "world"); String[] array = new String[]{"hello", "world"}; GeneralCopiable copiable = new GeneralCopiable(); } + private static class GeneralCopyException { + GeneralCopiableException copiable = new GeneralCopiableException(); + } + private static class GeneralCopiable { public GeneralCopiable copy() { @@ -51,6 +62,13 @@ public GeneralCopiable copy() { } } + private static class GeneralCopiableException { + + public GeneralCopiable copy() { + throw new IllegalArgumentException("Everything good"); + } + } + private static class CopyIterableImpl { } private static class CopyIterable implements Iterable { diff --git a/demo/build.gradle b/demo/build.gradle new file mode 100644 index 000000000..0e2f17a99 --- /dev/null +++ b/demo/build.gradle @@ -0,0 +1,40 @@ +plugins { + id 'groovy' + id 'application' + id 'com.github.johnrengelman.shadow' version '8.1.1' +} + +dependencies { + implementation libs.groovy + + compileOnly libs.spigot.latest + testImplementation libs.spigot.latest + testImplementation libs.jbukkit +} + +shadowJar { + exclude 'META-INF/**' + + archiveFileName = "${rootProject.name}-plugin-${project.version}.jar" + mainClassName = "${project.group}.${rootProject.name.toLowerCase()}.${rootProject.name}" +} + +jar { + dependsOn processResources + dependsOn shadowJar + + archiveFileName = "${project.name}-${project.version}-original.jar" +} + +processResources { + def props = [ + version: rootProject.version, name: rootProject.name, name_lower: rootProject.name.toLowerCase(), + description: rootProject.description, author: 'Fulminazzo', + group: rootProject.group, module: rootProject.name + ] + inputs.properties props + filteringCharset 'UTF-8' + filesMatching('*.yml') { + expand props + } +} \ No newline at end of file diff --git a/demo/src/main/groovy/it/angrybear/yagl/YAGL.groovy b/demo/src/main/groovy/it/angrybear/yagl/YAGL.groovy new file mode 100644 index 000000000..0c8912db0 --- /dev/null +++ b/demo/src/main/groovy/it/angrybear/yagl/YAGL.groovy @@ -0,0 +1,96 @@ +package it.angrybear.yagl + +import groovy.transform.CompileStatic +import it.angrybear.yagl.commands.ShellCommand +import it.angrybear.yagl.listeners.PersistentListener +import it.fulminazzo.fulmicollection.objects.Refl +import it.fulminazzo.fulmicollection.utils.JarUtils +import it.fulminazzo.yamlparser.utils.FileUtils +import org.bukkit.Bukkit +import org.bukkit.command.Command +import org.bukkit.command.CommandMap +import org.bukkit.plugin.java.JavaPlugin +import org.jetbrains.annotations.NotNull + +@CompileStatic +class YAGL extends JavaPlugin { + private final List commands = new ArrayList<>() + + @Override + void onEnable() { + loadCommands() + Bukkit.getPluginManager().registerEvents(new PersistentListener(), this) + getLogger().info("Loaded ${commands.size()} commands") + } + + @Override + void onDisable() { + unloadCommands() + } + + /** + * Loads all the commands from the {@link #getDataFolder()}/commands directory. + * If it does not exist, it is created using {@link #saveDefaultCommands(File)}. + */ + void loadCommands() { + this.commands.clear() + File commandsDir = new File(getDataFolder(), 'commands') + if (!commandsDir.exists()) saveDefaultCommands(commandsDir) + File[] files = commandsDir.listFiles() + if (files != null) + this.commands.addAll(files.findAll({ it.name.endsWith(".groovy") }).collect { new ShellCommand(it) }) + + commandMap().ifPresent { map -> this.commands.each { map.register(getName(), it) } } + } + + /** + * Unloads all the commands loaded in {@link #commands}. + */ + void unloadCommands() { + commandMap().ifPresent(map -> { + Map commands = new Refl<>(map).getFieldObject('knownCommands') + if (commands == null) getLogger().warning('Could not find \'knownCommands\' field in CommandMap') + else commands.keySet().collect().each { key -> + Command value = commands.get(key) + if (this.commands.contains(value)) commands.remove(key, value) + } + }) + } + + private static Optional commandMap() { + def pluginManager = Bukkit.getPluginManager() + // Terrible line, but necessary for JaCoCo coverage report to 100% + pluginManager == null ? Optional.empty() : Optional.ofNullable((CommandMap) new Refl<>(pluginManager) + .getFieldObject('commandMap')) + } + + /** + * Saves all the default command scripts to the given directory + * + * @param commandsDir the output directory + */ + void saveDefaultCommands(final @NotNull File commandsDir) { + final def resourceDir = '/commands' + FileUtils.createFolder(commandsDir) + getClass().getResourceAsStream(resourceDir).withReader { reader -> + String fileName + while ((fileName = reader.readLine()) != null) writeResourceToFile(commandsDir, fileName, resourceDir) + } + Iterator jarEntries = JarUtils.getEntries(YAGL, "") + while (jarEntries.hasNext()) { + def entry = jarEntries.next() + if (entry.startsWith(resourceDir.substring(1)) && entry.length() > resourceDir.length()) + writeResourceToFile(commandsDir, entry.substring(resourceDir.length()), resourceDir) + } + } + + private void writeResourceToFile(final @NotNull File dir, final @NotNull String fileName, final @NotNull resourceDir) { + def file = new File(dir, fileName) + if (file.exists()) FileUtils.deleteFile(file) + FileUtils.createNewFile(file) + def input = getClass().getResourceAsStream("${resourceDir}/${fileName}") + def output = new FileOutputStream(file) + output << input + } + +} diff --git a/demo/src/main/groovy/it/angrybear/yagl/commands/ShellCommand.groovy b/demo/src/main/groovy/it/angrybear/yagl/commands/ShellCommand.groovy new file mode 100644 index 000000000..d89db5484 --- /dev/null +++ b/demo/src/main/groovy/it/angrybear/yagl/commands/ShellCommand.groovy @@ -0,0 +1,51 @@ +package it.angrybear.yagl.commands + +import groovy.transform.CompileStatic +import it.fulminazzo.yamlparser.utils.FileUtils +import org.bukkit.command.Command +import org.bukkit.command.CommandSender +import org.jetbrains.annotations.NotNull + +import java.util.regex.Pattern + +/** + * A general class used to create a command from a Groovy script. + */ +@CompileStatic +class ShellCommand extends Command { + private static final String NUMBER_FORMAT_REGEX = '(catch *\\(NumberFormatException +ignored\\) *\\{\\n)[ \\t]*(\\n *})' + private static final String INVALID_NUMBER_CODE = 'sender.sendMessage(e.getMessage().replace(\'For input string: \', \'Invalid number \'))' + private final String shellCode + + /** + * Instantiates a new shell command + * + * @param file the file containing the script + */ + ShellCommand(final @NotNull File file) { + super(file.getName().substring(0, file.getName().lastIndexOf('.'))) + def code = FileUtils.readFileToString(file) + if (code == null) { + this.shellCode = '' + return + } + def matcher = Pattern.compile(NUMBER_FORMAT_REGEX).matcher(code) + while (matcher.find()) { + def replacement = "${matcher.group(1)} ${INVALID_NUMBER_CODE}${matcher.group(2)}" + code = code.replace(matcher.group(), replacement) + } + this.shellCode = "${code}\nrun(sender, label, args)" + } + + @Override + boolean execute(@NotNull CommandSender sender, @NotNull String label, @NotNull String[] args) { + Binding binding = new Binding(["sender": sender, "label": label, "args": args]) + new GroovyShell(binding).evaluate(this.shellCode) + return true + } + + @Override + List tabComplete(@NotNull CommandSender sender, @NotNull String alias, @NotNull String[] args) throws IllegalArgumentException { + return new ArrayList<>() + } +} diff --git a/demo/src/main/resources/commands/ApplyPotionEffect.groovy b/demo/src/main/resources/commands/ApplyPotionEffect.groovy new file mode 100644 index 000000000..b0f20a0f5 --- /dev/null +++ b/demo/src/main/resources/commands/ApplyPotionEffect.groovy @@ -0,0 +1,20 @@ +import it.angrybear.yagl.WrappersAdapter +import it.angrybear.yagl.wrappers.PotionEffect +import org.bukkit.entity.Player + +def run = { sender, label, args -> + if (sender instanceof Player) { + try { + PotionEffect effect = new PotionEffect(args[0], Double.valueOf(args[1]), + Integer.valueOf(args[2]), Boolean.valueOf(args[3]), Boolean.valueOf(args[4])) + def potionEffect = WrappersAdapter.wPotionEffectToPotionEffect(effect) + sender.addPotionEffect(potionEffect) + } catch (IndexOutOfBoundsException ignored) { + sender.sendMessage('Usage: /applypotioneffect ') + } catch (NumberFormatException ignored) { + + } catch (Exception e) { + sender.sendMessage(e.getMessage()) + } + } else sender.sendMessage('Console cannot execute this command!') +} \ No newline at end of file diff --git a/demo/src/main/resources/commands/CreateRecipe.groovy b/demo/src/main/resources/commands/CreateRecipe.groovy new file mode 100644 index 000000000..274656b65 --- /dev/null +++ b/demo/src/main/resources/commands/CreateRecipe.groovy @@ -0,0 +1,70 @@ +import it.angrybear.yagl.items.BukkitItem +import it.angrybear.yagl.items.Item +import it.angrybear.yagl.items.recipes.FurnaceRecipe +import it.angrybear.yagl.items.recipes.ShapedRecipe +import it.angrybear.yagl.items.recipes.ShapelessRecipe +import org.bukkit.entity.Player + +def shaped(sender, label, args, output, name) { + try { + def rows = Integer.valueOf(args[0]) + def columns = Integer.valueOf(args[1]) + def recipe = new ShapedRecipe(name).setShape(rows, columns) + if (args.length < 2) throw new IndexOutOfBoundsException() + for (int i = 2; i < args.length; i += 1) + recipe.setIngredient(i - 2, Item.newItem(args[i])) + BukkitItem.newRecipeItem(output).addRecipes(recipe).registerRecipes() + sender.sendMessage('Ok') + } catch (NumberFormatException ignored) { + + } catch (IndexOutOfBoundsException ignored) { + sender.sendMessage('Usage: /createrecipe shaped ') + } +} + +def shapeless(sender, label, args, output, name) { + try { + if (args.length == 0) throw new IndexOutOfBoundsException() + BukkitItem.newRecipeItem(output) + .addRecipes(new ShapelessRecipe(name) + .addIngredients(Arrays.stream(args) + .map(Item::newItem) + .toArray(Item[]::new))) + .registerRecipes() + sender.sendMessage('Ok') + } catch (NumberFormatException ignored) { + + } catch (IndexOutOfBoundsException ignored) { + sender.sendMessage('Usage: /createrecipe shapeless ') + } +} + +def furnace(sender, label, args, output, name) { + try { + BukkitItem.newRecipeItem(output) + .addRecipes(new FurnaceRecipe(name) + .setIngredient(Item.newItem(args[0])) + .setCookingTime(Double.valueOf(args[1])) + .setExperience(Float.valueOf(args[2]))) + .registerRecipes() + sender.sendMessage('Ok') + } catch (NumberFormatException ignored) { + + } catch (IndexOutOfBoundsException ignored) { + sender.sendMessage('Usage: /createrecipe furnace ') + } +} + +def run = { sender, label, args -> + if (sender instanceof Player) + try { + "${args[2].toLowerCase()}"(sender, label, Arrays.copyOfRange(args, 3, args.length), args[1], args[0]) + } catch (NumberFormatException ignored) { + + } catch (IndexOutOfBoundsException ignored) { + sender.sendMessage('Usage: /createrecipe ') + } catch (Exception e) { + sender.sendMessage(e.getMessage()) + } + else sender.sendMessage('Console cannot execute this command!') +} \ No newline at end of file diff --git a/demo/src/main/resources/commands/GetEnchantment.groovy b/demo/src/main/resources/commands/GetEnchantment.groovy new file mode 100644 index 000000000..e687593e2 --- /dev/null +++ b/demo/src/main/resources/commands/GetEnchantment.groovy @@ -0,0 +1,25 @@ +import it.angrybear.yagl.WrappersAdapter +import it.angrybear.yagl.wrappers.Enchantment +import it.fulminazzo.fulmicollection.structures.tuples.Tuple +import org.bukkit.Material +import org.bukkit.entity.Player +import org.bukkit.inventory.ItemStack +import org.bukkit.inventory.meta.EnchantmentStorageMeta + +def run = { sender, label, args -> + if (sender instanceof Player) + try { + Enchantment enchantment = new Enchantment(args[0], Integer.valueOf(args[1])) + ItemStack book = new ItemStack(Material.ENCHANTED_BOOK) + EnchantmentStorageMeta meta = book.getItemMeta() + Tuple tuple = WrappersAdapter.wEnchantToEnchant(enchantment) + meta.addStoredEnchant(tuple.getKey(), tuple.getValue(), true) + book.setItemMeta(meta) + sender.getInventory().addItem(book) + } catch (NumberFormatException ignored) { + + } catch (IndexOutOfBoundsException ignored) { + sender.sendMessage('Usage: /getenchantment ') + } + else sender.sendMessage('Console cannot execute this command!') +} \ No newline at end of file diff --git a/demo/src/main/resources/commands/GiveItem.groovy b/demo/src/main/resources/commands/GiveItem.groovy new file mode 100644 index 000000000..4314a3966 --- /dev/null +++ b/demo/src/main/resources/commands/GiveItem.groovy @@ -0,0 +1,36 @@ +import it.angrybear.yagl.items.BukkitItem +import it.angrybear.yagl.items.Item +import it.angrybear.yagl.items.fields.ItemFlag +import it.angrybear.yagl.wrappers.Enchantment +import it.angrybear.yagl.wrappers.WrapperParser +import org.bukkit.entity.Player + +def run = { sender, label, args -> + if (sender instanceof Player) + try { + Item item = Item.newItem(args[0]) + try { + item.setAmount(Integer.valueOf(args[1])) + .setDurability(Integer.valueOf(args[2])) + .setDisplayName(args[3]) + .setLore(args[4].split(";")) + .addEnchantments(Arrays.stream(args[5].split(";")) + .map(a -> WrapperParser.parseWrapperFromString(a, Enchantment)) + .toArray(Enchantment[]::new)) + .addItemFlags(Arrays.stream(args[6].split(";")) + .map(a -> ItemFlag.valueOf(a.toUpperCase())) + .toArray(ItemFlag[]::new)) + .setUnbreakable(Boolean.valueOf(args[7])) + .setCustomModelData(Integer.valueOf(args[8])) + } catch (IndexOutOfBoundsException ignored) { + + } + sender.getInventory().addItem(item.copy(BukkitItem).create()) + } catch (NumberFormatException ignored) { + + } catch (IndexOutOfBoundsException ignored) { + sender.sendMessage('Usage: /giveitem ') + sender.sendMessage('At least material is required!') + } + else sender.sendMessage('Console cannot execute this command!') +} \ No newline at end of file diff --git a/demo/src/main/resources/commands/GivePersistentItem.groovy b/demo/src/main/resources/commands/GivePersistentItem.groovy new file mode 100644 index 000000000..aa3359f6a --- /dev/null +++ b/demo/src/main/resources/commands/GivePersistentItem.groovy @@ -0,0 +1,41 @@ +import it.angrybear.yagl.items.BukkitItem +import it.angrybear.yagl.items.DeathAction +import it.angrybear.yagl.items.Item +import it.angrybear.yagl.items.Mobility +import it.angrybear.yagl.items.PersistentItem +import it.angrybear.yagl.items.fields.ItemFlag +import it.angrybear.yagl.wrappers.Enchantment +import it.angrybear.yagl.wrappers.WrapperParser +import org.bukkit.entity.Player + +def run = { sender, label, args -> + if (sender instanceof Player) + try { + DeathAction action = DeathAction.valueOf(args[0].toUpperCase()) + Mobility mobility = Mobility.valueOf(args[1].toUpperCase()) + PersistentItem item = PersistentItem.newItem(args[2]).setDeathAction(action).setMobility(mobility) + try { + item.setAmount(Integer.valueOf(args[3])) + .setDurability(Integer.valueOf(args[4])) + .setDisplayName(args[5]) + .setLore(args[6].split(";")) + .addEnchantments(Arrays.stream(args[7].split(";")) + .map(a -> WrapperParser.parseWrapperFromString(a, Enchantment)) + .toArray(Enchantment[]::new)) + .addItemFlags(Arrays.stream(args[8].split(";")) + .map(a -> ItemFlag.valueOf(a.toUpperCase())) + .toArray(ItemFlag[]::new)) + .setUnbreakable(Boolean.valueOf(args[9])) + .setCustomModelData(Integer.valueOf(args[10])) + } catch (IndexOutOfBoundsException ignored) { + + } + sender.getInventory().addItem(item.copy(BukkitItem).create()) + } catch (NumberFormatException ignored) { + + } catch (IndexOutOfBoundsException ignored) { + sender.sendMessage('Usage: /givepersistentitem ') + sender.sendMessage('At least death-action, mobility and material are required!') + } + else sender.sendMessage('Console cannot execute this command!') +} \ No newline at end of file diff --git a/demo/src/main/resources/commands/OpenGUI.groovy b/demo/src/main/resources/commands/OpenGUI.groovy new file mode 100644 index 000000000..b39791637 --- /dev/null +++ b/demo/src/main/resources/commands/OpenGUI.groovy @@ -0,0 +1,58 @@ +import it.angrybear.yagl.GUIManager +import it.angrybear.yagl.contents.ItemGUIContent +import it.angrybear.yagl.guis.GUI +import it.angrybear.yagl.guis.GUIType +import it.angrybear.yagl.items.Item +import it.angrybear.yagl.items.fields.ItemFlag +import it.angrybear.yagl.utils.EnumUtils +import org.bukkit.entity.Player + +def run = { sender, label, args -> + if (sender instanceof Player) + try { + def columns = 9 + def border = Item.newItem("black_stained_glass_pane").setDisplayName(" ") + GUI gui + try { + gui = GUI.newGUI(EnumUtils.valueOf(GUIType, args[0])) + } catch (IllegalArgumentException ignored) { + gui = GUI.newResizableGUI(Integer.valueOf(args[0])) + } catch (IndexOutOfBoundsException ignored) { + sender.sendMessage("Usage: /opengui ") + return + } + for (int i = 0; i < Math.min(gui.size(), columns); i += 1) { + gui.setContents(i, border) + gui.setContents(gui.size() - i - 1, border) + } + for (int i = 0; i < gui.size(); i += columns) { + gui.setContents(i, border) + gui.setContents(gui.size() - i - 1, border) + } + def middle = (int) (gui.size() / 2) + gui.setContents(middle, Item.newItem("gold_block").setDisplayName("This is a GUI!") + .addEnchantment("unbreaking", 1) + .addItemFlags(ItemFlag.HIDE_ENCHANTS)) + if (middle - 1 >= 0) + gui.setContents(middle - 1, Item.newItem("diamond_sword") + .setDisplayName("Pick me!") + .addEnchantment("sharpness", 2)) + .setMovable(middle - 1, true) + if (middle + 1 < gui.size()) + gui.setContents(middle + 1, ItemGUIContent.newInstance("diamond_pickaxe") + .setDisplayName("Can't pick me...") + .addEnchantment("efficiency", 10) + .onClickItem((v, g, c) -> v.sendMessage('You cannot pick this item!'))) + gui.setTitle(" GUI") + .onClickOutside((v, g) -> v.sendMessage('Please only click inside me!')) + .onOpenGUI((v, g) -> v.sendMessage('Opening the GUI... voila')) + .onCloseGUI((v, g) -> v.sendMessage('Goodbye!')) + .setVariable("name", "Demo") + .open(GUIManager.getViewer(sender)) + } catch (NumberFormatException ignored) { + + } catch (Exception e) { + sender.sendMessage(e.getMessage()) + } + else sender.sendMessage('Console cannot execute this command!') +} \ No newline at end of file diff --git a/demo/src/main/resources/commands/OpenPageableGUI.groovy b/demo/src/main/resources/commands/OpenPageableGUI.groovy new file mode 100644 index 000000000..1c16fcb39 --- /dev/null +++ b/demo/src/main/resources/commands/OpenPageableGUI.groovy @@ -0,0 +1,43 @@ +import it.angrybear.yagl.GUIManager +import it.angrybear.yagl.guis.GUIType +import it.angrybear.yagl.guis.PageableGUI +import it.angrybear.yagl.items.BukkitItem +import it.angrybear.yagl.utils.EnumUtils +import org.bukkit.Material +import org.bukkit.entity.Player + +def run = { sender, label, args -> + if (sender instanceof Player) + try { + PageableGUI gui + try { + gui = PageableGUI.newGUI(EnumUtils.valueOf(GUIType, args[0])).setPages(Integer.valueOf(args[1])) + } catch (IllegalArgumentException ignored) { + gui = PageableGUI.newGUI(Integer.valueOf(args[0])).setPages(Integer.valueOf(args[1])) + } catch (IndexOutOfBoundsException ignored) { + sender.sendMessage("Usage: /openpageablegui ") + return + } + def size = gui.size() + def middle = (int) Math.min(size / 2, 9 / 2) + if (size > 1) { + size -= 1 + gui.setContents(size - middle, BukkitItem.newItem(Material.OBSIDIAN).setDisplayName("&7Page: &e")) + .setPreviousPage(size - middle * 2, BukkitItem.newItem(Material.REDSTONE_BLOCK) + .setDisplayName("&7Go to page &e")) + .setNextPage(size, BukkitItem.newItem(Material.EMERALD_BLOCK) + .setDisplayName("&7Go to page &e")) + } + + gui.setTitle("Page #") + .onClickOutside((v, g) -> v.sendMessage('Please only click inside me!')) + .onOpenGUI((v, g) -> v.sendMessage(g.apply('Opening page '))) + .onCloseGUI((v, g) -> v.sendMessage('Goodbye!')) + .open(GUIManager.getViewer(sender)) + } catch (NumberFormatException ignored) { + + } catch (Exception e) { + sender.sendMessage(e.getMessage()) + } + else sender.sendMessage('Console cannot execute this command!') +} \ No newline at end of file diff --git a/demo/src/main/resources/commands/PlayEffect.groovy b/demo/src/main/resources/commands/PlayEffect.groovy new file mode 100644 index 000000000..2b75c516f --- /dev/null +++ b/demo/src/main/resources/commands/PlayEffect.groovy @@ -0,0 +1,56 @@ +/** + * Spawns the specified particle at the player's eyes location. + * If additional arguments are specified, and the particle supports it, + * they are converted to the corresponding ParticleOption. + * See {@link #getOption(Object, Object, Object, Object)} to find out how. + */ +import it.angrybear.yagl.Color +import it.angrybear.yagl.WrappersAdapter +import it.angrybear.yagl.particles.* +import it.angrybear.yagl.wrappers.Potion +import it.fulminazzo.fulmicollection.objects.Refl +import org.bukkit.entity.Player + +def getOption(sender, particleType, optionType, args) { + if (optionType == PotionParticleOption) + new PotionParticleOption(new Potion(args[0], Integer.valueOf(args[1]), + Boolean.valueOf(args[2]), Boolean.valueOf(args[3]))) + else if (optionType == MaterialDataOption) + new MaterialDataOption(args[0]) + else if (optionType == ColorParticleOption) + new ColorParticleOption(Color.fromARGB(args[0])) + else if (particleType == LegacyParticleType.SMOKE) + new PrimitiveParticleOption<>(args[0]) + else if (particleType == LegacyParticleType.VILLAGER_PLANT_GROW) + new PrimitiveParticleOption<>(Integer.valueOf(args[0])) + else if (particleType == LegacyParticleType.ITEM_BREAK) + new PrimitiveParticleOption<>(args[0]) + else if (particleType == LegacyParticleType.COMPOSTER_FILL_ATTEMPT) + new PrimitiveParticleOption<>(Boolean.valueOf(args[0])) + else if (particleType == LegacyParticleType.BONE_MEAL_USE) + new PrimitiveParticleOption<>(Integer.valueOf(args[0])) + else if (particleType == LegacyParticleType.ELECTRIC_SPARK) + new PrimitiveParticleOption<>(args[0]) + else throw new IllegalArgumentException("Cannot get particle option of ${optionType}") +} + +def run = { sender, label, args -> + if (sender instanceof Player) { + try { + LegacyParticleType type = LegacyParticleType.valueOf(args[0]) + Class optionType = new Refl<>(type).getFieldObject('optionType') + Particle particle + if (args.length > 1 && optionType != null) { + def option = getOption(sender, type, optionType, Arrays.copyOfRange(args, 1, args.length)) + particle = type.create(option) + } else particle = type.create() + WrappersAdapter.spawnEffect(sender, particle, sender.getEyeLocation()) + } catch (IndexOutOfBoundsException ignored) { + sender.sendMessage('Usage: /playeffect ') + } catch (NumberFormatException ignored) { + + } catch (Exception e) { + sender.sendMessage(e.getMessage()) + } + } else sender.sendMessage('Console cannot execute this command!') +} \ No newline at end of file diff --git a/demo/src/main/resources/commands/PlayParticle.groovy b/demo/src/main/resources/commands/PlayParticle.groovy new file mode 100644 index 000000000..e4d1ec6b8 --- /dev/null +++ b/demo/src/main/resources/commands/PlayParticle.groovy @@ -0,0 +1,56 @@ +/** + * Spawns the specified particle at the player's eyes location. + * If additional arguments are specified, and the particle supports it, + * they are converted to the corresponding ParticleOption. + * See {@link #getOption(Object, Object, Object, Object)} to find out how. + */ +import it.angrybear.yagl.Color +import it.angrybear.yagl.ItemAdapter +import it.angrybear.yagl.WrappersAdapter +import it.angrybear.yagl.particles.* +import it.fulminazzo.fulmicollection.objects.Refl +import org.bukkit.Location +import org.bukkit.entity.Player +import org.bukkit.inventory.EquipmentSlot + +def getOption(sender, particleType, optionType, args) { + if (optionType == DustParticleOption) + new DustParticleOption(Color.fromARGB(args[0]), Float.valueOf(args[1])) + if (optionType == DustTransitionParticleOption) + new DustTransitionParticleOption(Color.fromARGB(args[0]), Color.fromARGB(args[1]), Float.valueOf(args[2])) + else if (optionType == ItemParticleOption) + ItemAdapter.itemStackToItem(sender.inventory.getItem(EquipmentSlot.HAND)) + else if (optionType == BlockDataOption) + new BlockDataOption(args[0]) + else if (particleType == ParticleType.VIBRATION) { + Location start = sender.location + Location end = start.clone().add(0, 10, 0) + def dest = new org.bukkit.Vibration.Destination.BlockDestination(end) + new PrimitiveParticleOption<>(new org.bukkit.Vibration(start, dest, Integer.valueOf(args[0]))) + } else if (particleType == ParticleType.SCULK_CHARGE) + new PrimitiveParticleOption<>(Float.valueOf(args[0])) + else if (particleType == ParticleType.SHRIEK) + new PrimitiveParticleOption<>(Integer.valueOf(args[0])) + else throw new IllegalArgumentException("Cannot get particle option of ${optionType}") +} + +def run = { sender, label, args -> + if (sender instanceof Player) { + try { + ParticleType type = ParticleType.valueOf(args[0]) + Class optionType = new Refl<>(type).getFieldObject('optionType') + Particle particle + if (args.length > 1 && optionType != null) { + def option = getOption(sender, type, optionType, Arrays.copyOfRange(args, 1, args.length)) + particle = type.create(option) + } else particle = type.create() + WrappersAdapter.spawnParticle(sender, particle, sender.getEyeLocation(), 1) + } catch (IndexOutOfBoundsException ignored) { + sender.sendMessage('Usage: /playparticle ') + } catch (NumberFormatException ignored) { + + } catch (Exception e) { + sender.sendMessage(e.getMessage()) + } + } else sender.sendMessage('Console cannot execute this command!') +} \ No newline at end of file diff --git a/demo/src/main/resources/commands/PlayYAGLCustomSound.groovy b/demo/src/main/resources/commands/PlayYAGLCustomSound.groovy new file mode 100644 index 000000000..fb4143c72 --- /dev/null +++ b/demo/src/main/resources/commands/PlayYAGLCustomSound.groovy @@ -0,0 +1,18 @@ +import it.angrybear.yagl.WrappersAdapter +import it.angrybear.yagl.wrappers.Sound +import org.bukkit.entity.Player + +def run = { sender, label, args -> + if (sender instanceof Player) { + try { + Sound sound = new Sound(args[0], Float.valueOf(args[1]), Float.valueOf(args[2]), args[3]) + WrappersAdapter.playCustomSound(sender, sound) + } catch (IndexOutOfBoundsException ignored) { + sender.sendMessage('Usage: /playsound ') + } catch (NumberFormatException ignored) { + + } catch (Exception e) { + sender.sendMessage(e.getMessage()) + } + } else sender.sendMessage('Console cannot execute this command!') +} \ No newline at end of file diff --git a/demo/src/main/resources/commands/PlayYAGLSound.groovy b/demo/src/main/resources/commands/PlayYAGLSound.groovy new file mode 100644 index 000000000..d7600a62f --- /dev/null +++ b/demo/src/main/resources/commands/PlayYAGLSound.groovy @@ -0,0 +1,18 @@ +import it.angrybear.yagl.WrappersAdapter +import it.angrybear.yagl.wrappers.Sound +import org.bukkit.entity.Player + +def run = { sender, label, args -> + if (sender instanceof Player) { + try { + Sound sound = new Sound(args[0], Float.valueOf(args[1]), Float.valueOf(args[2]), args[3]) + WrappersAdapter.playSound(sender, sound) + } catch (IndexOutOfBoundsException ignored) { + sender.sendMessage('Usage: /playsound ') + } catch (NumberFormatException ignored) { + + } catch (Exception e) { + sender.sendMessage(e.getMessage()) + } + } else sender.sendMessage('Console cannot execute this command!') +} \ No newline at end of file diff --git a/demo/src/main/resources/plugin.yml b/demo/src/main/resources/plugin.yml new file mode 100644 index 000000000..4b3349e30 --- /dev/null +++ b/demo/src/main/resources/plugin.yml @@ -0,0 +1,6 @@ +name: "${name}" +description: "${description}" +version: "${version}" +authors: ["${author}"] +main: "${group}.${name_lower}.${name}" +api-version: 1.13 diff --git a/demo/src/test/java/it/angrybear/yagl/YAGLTest.java b/demo/src/test/java/it/angrybear/yagl/YAGLTest.java new file mode 100644 index 000000000..30bc9b065 --- /dev/null +++ b/demo/src/test/java/it/angrybear/yagl/YAGLTest.java @@ -0,0 +1,193 @@ +package it.angrybear.yagl; + +import it.angrybear.yagl.commands.ShellCommand; +import it.fulminazzo.fulmicollection.objects.Refl; +import it.fulminazzo.fulmicollection.utils.JarUtils; +import it.fulminazzo.jbukkit.BukkitUtils; +import it.fulminazzo.yamlparser.utils.FileUtils; +import org.bukkit.Bukkit; +import org.bukkit.Server; +import org.bukkit.command.Command; +import org.bukkit.command.CommandMap; +import org.bukkit.command.SimpleCommandMap; +import org.bukkit.plugin.PluginManager; +import org.bukkit.plugin.SimplePluginManager; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.util.*; +import java.util.logging.*; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class YAGLTest { + private YAGL plugin; + private Handler handler; + private ByteArrayOutputStream output; + + @BeforeEach + void setUp() throws IOException { + BukkitUtils.setupServer(); + + this.output = new ByteArrayOutputStream(); + this.handler = new StreamHandler(this.output, new SimpleFormatter()); + this.handler.setLevel(Level.ALL); + Logger logger = Logger.getAnonymousLogger(); + logger.addHandler(handler); + + this.plugin = mock(YAGL.class); + File dataDir = new File("build/resources/test/plugin"); + if (dataDir.exists()) FileUtils.deleteFolder(dataDir); + when(this.plugin.getDataFolder()).thenReturn(dataDir); + new Refl<>(this.plugin).setFieldObject("commands", new LinkedList<>()); + when(this.plugin.getName()).thenReturn("YAGL-Plugin"); + doCallRealMethod().when(this.plugin).loadCommands(); + doCallRealMethod().when(this.plugin).unloadCommands(); + doCallRealMethod().when(this.plugin).saveDefaultCommands(any()); + when(this.plugin.getLogger()).thenReturn(logger); + } + + @Test + void testOnEnableShouldCallLoadCommands() { + Server server = mock(Server.class); + when(server.getPluginManager()).thenReturn(mock(PluginManager.class)); + new Refl<>(Bukkit.class).setFieldObject("server", server); + YAGL plugin = mock(YAGL.class); + when(plugin.getLogger()).thenReturn(Logger.getAnonymousLogger()); + new Refl<>(plugin).setFieldObject("commands", new ArrayList<>()); + doCallRealMethod().when(plugin).onEnable(); + plugin.onEnable(); + verify(plugin, atLeastOnce()).loadCommands(); + } + + @Test + void testOnDisableShouldCallUnloadCommands() { + YAGL plugin = mock(YAGL.class); + doCallRealMethod().when(plugin).onDisable(); + plugin.onDisable(); + verify(plugin, atLeastOnce()).unloadCommands(); + } + + @Test + void testLoadCommands() { + this.plugin.loadCommands(); + List commands = new Refl<>(this.plugin).getFieldObject("commands"); + assertNotNull(commands); + assertEquals(1, commands.size(), "Expected one command"); + ShellCommand command = commands.get(0); + assertEquals("mock", command.getName()); + } + + @Test + void testLoadCommandsFromAlreadyPresentDirectory() throws IOException { + FileUtils.createFolder(new File(this.plugin.getDataFolder(), "commands")); + this.plugin.loadCommands(); + List commands = new Refl<>(this.plugin).getFieldObject("commands"); + assertNotNull(commands); + assertEquals(0, commands.size(), "Expected zero commands"); + } + + @Test + void testSaveDefaultCommands() throws IOException { + final String fileName = "commands/mock.groovy"; + File expected = new File(this.plugin.getDataFolder(), fileName); + String expectedContent = FileUtils.readFileToString(new File("build/resources/test/" + fileName)); + + this.plugin.saveDefaultCommands(expected.getParentFile()); + + assertTrue(expected.exists(), String.format("Expected file '%s' to exist", expected.getAbsolutePath())); + String actualContent = FileUtils.readFileToString(expected); + + assertEquals(expectedContent, actualContent, "Content did not match expected"); + } + + @Test + void testLoadAndRegisterCommands() { + setupPluginManager(); + CommandMap commandMap = new Refl<>(Bukkit.getPluginManager()).getFieldObject("commandMap"); + assertNotNull(commandMap); + assertNull(commandMap.getCommand("mock"), "Not expected 'mock' command but some was found"); + this.plugin.loadCommands(); + assertNotNull(commandMap.getCommand("mock"), "Expected 'mock' command but none was found"); + } + + @Test + void testUnloadAndUnregisterCommands() { + setupPluginManager(); + CommandMap commandMap = new Refl<>(Bukkit.getPluginManager()).getFieldObject("commandMap"); + assertNotNull(commandMap); + + ShellCommand shellCommand = new ShellCommand(new File(this.plugin.getDataFolder(), "commands/mock.groovy")); + Map knownCommands = new Refl<>(commandMap).getFieldObject("knownCommands"); + assertNotNull(knownCommands); + knownCommands.put("mock", shellCommand); + + List commands = new Refl<>(this.plugin).getFieldObject("commands"); + assertNotNull(commands); + commands.add(shellCommand); + + assertNotNull(commandMap.getCommand("mock"), "Expected 'mock' command but none was found"); + this.plugin.unloadCommands(); + assertNull(commandMap.getCommand("mock"), "Not expected 'mock' command but some was found"); + } + + @Test + void testInvalidCommandsDirectory() throws IOException { + File file = new File(this.plugin.getDataFolder(), "commands"); + if (file.isFile()) FileUtils.deleteFile(file); + if (file.isDirectory()) FileUtils.deleteFolder(file); + FileUtils.createNewFile(file); + assertDoesNotThrow(() -> this.plugin.loadCommands()); + file.delete(); + } + + @Test + void testNullKnownCommands() { + final String field = "knownCommands"; + + setupPluginManager(); + new Refl<>(Bukkit.getPluginManager()).getFieldRefl("commandMap").setFieldObject(field, null); + + assertDoesNotThrow(() -> this.plugin.unloadCommands()); + + String printOutput = getOutput(); + assertTrue(printOutput.contains(field), String.format("'%s' did not contain '%s'", printOutput, field)); + } + + @Test + void testNullPluginManager() { + assertDoesNotThrow(() -> this.plugin.loadCommands()); + } + + @Test + void simulateJarEntries() { + setupPluginManager(); + List entries = Arrays.asList("ignored", "also-ignored", "commands", "commands/", "commands/mock.groovy"); + try (MockedStatic ignored = mockStatic(JarUtils.class)) { + when(JarUtils.getEntries((Class) any(), anyString())).thenAnswer(a -> entries.iterator()); + + this.plugin.loadCommands(); + + List commands = new Refl<>(this.plugin).getFieldObject("commands"); + assertNotNull(commands); + assertEquals(1, commands.size(), "Commands size did not match expected"); + } + } + + private void setupPluginManager() { + Server server = Bukkit.getServer(); + SimpleCommandMap commandMap = new SimpleCommandMap(server); + SimplePluginManager pluginManager = new SimplePluginManager(server, commandMap); + when(server.getPluginManager()).thenReturn(pluginManager); + } + + private String getOutput() { + this.handler.flush(); + return this.output.toString(); + } +} \ No newline at end of file diff --git a/demo/src/test/java/it/angrybear/yagl/commands/ShellCommandTest.java b/demo/src/test/java/it/angrybear/yagl/commands/ShellCommandTest.java new file mode 100644 index 000000000..719568c38 --- /dev/null +++ b/demo/src/test/java/it/angrybear/yagl/commands/ShellCommandTest.java @@ -0,0 +1,60 @@ +package it.angrybear.yagl.commands; + +import it.fulminazzo.fulmicollection.objects.Refl; +import org.bukkit.command.CommandSender; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.IOException; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.*; + +class ShellCommandTest { + + @Test + void testExecute() { + // Prepare file + File file = new File("build/resources/test/shell-command.groovy"); + // Prepare sender + AtomicReference message = new AtomicReference<>(); + CommandSender sender = mock(CommandSender.class); + doAnswer(a -> { + message.set(a.getArgument(0)); + return null; + }).when(sender).sendMessage(anyString()); + + ShellCommand shellCommand = new ShellCommand(file); + shellCommand.execute(sender, "command", new String[0]); + + assertEquals("Hello world", message.get()); + } + + @Test + void testNumberFormatExceptionReplacement() { + String expected = "def run = { sender, label, args ->\n" + + " try {\n" + + " sender.sendMessage(\"Your number is ${Integer.valueOf(args[0])}\")\n" + + " } catch (NumberFormatException ignored) {\n" + + " sender.sendMessage(e.getMessage().replace('For input string: ', 'Invalid number '))\n" + + " }\n" + + "}\n" + + "run(sender, label, args)"; + File file = new File("build/resources/test/number-format-exception-command.groovy"); + + ShellCommand shellCommand = new ShellCommand(file); + String actual = new Refl<>(shellCommand).getFieldObject("shellCode"); + + assertEquals(expected, actual); + } + + @Test + void testTabCompleteShouldNotBeNull() throws IOException { + File file = File.createTempFile("build/resources/test", "file.groovy"); + assertNotNull(new ShellCommand(file).tabComplete(mock(CommandSender.class), + "command", new String[0])); + } + +} \ No newline at end of file diff --git a/demo/src/test/resources/commands/mock.groovy b/demo/src/test/resources/commands/mock.groovy new file mode 100644 index 000000000..f9cab74f1 --- /dev/null +++ b/demo/src/test/resources/commands/mock.groovy @@ -0,0 +1 @@ +def str = 'content' \ No newline at end of file diff --git a/demo/src/test/resources/number-format-exception-command.groovy b/demo/src/test/resources/number-format-exception-command.groovy new file mode 100644 index 000000000..ab77e553d --- /dev/null +++ b/demo/src/test/resources/number-format-exception-command.groovy @@ -0,0 +1,7 @@ +def run = { sender, label, args -> + try { + sender.sendMessage("Your number is ${Integer.valueOf(args[0])}") + } catch (NumberFormatException ignored) { + + } +} \ No newline at end of file diff --git a/demo/src/test/resources/shell-command.groovy b/demo/src/test/resources/shell-command.groovy new file mode 100644 index 000000000..aa6ca73a2 --- /dev/null +++ b/demo/src/test/resources/shell-command.groovy @@ -0,0 +1,3 @@ +def run = { sender, label, args -> + sender.sendMessage("Hello world") +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 589902775..cafc6391a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,8 +1,9 @@ [versions] delombok = "8.4" +groovy = "4.0.16" -fulmicollection = "1.5.2" -yamlparser = "1.6.2" +fulmicollection = "1.6" +yamlparser = "1.6.3" spigot = "1.14-R0.1-SNAPSHOT" spigot_obsolete = "1.8.8-R0.1-SNAPSHOT" @@ -17,6 +18,7 @@ mockito = "4.11.0" jbukkit = "2.1" [libraries] +groovy = { module = "org.apache.groovy:groovy", version.ref = "groovy" } fulmicollection = { module = "it.fulminazzo:FulmiCollection", version.ref = "fulmicollection" } yamlparser = { module = "it.fulminazzo:YAMLParser", version.ref = "yamlparser" } diff --git a/gui/base/src/main/java/it/angrybear/yagl/Metadatable.java b/gui/base/src/main/java/it/angrybear/yagl/Metadatable.java index 84de5885f..01de39a62 100644 --- a/gui/base/src/main/java/it/angrybear/yagl/Metadatable.java +++ b/gui/base/src/main/java/it/angrybear/yagl/Metadatable.java @@ -123,15 +123,15 @@ default boolean hasVariable(final @NotNull String name) { @SuppressWarnings("unchecked") default T apply(final T object) { if (object == null) return null; + else if (object.getClass().isEnum()) return object; else if (object instanceof String) return (T) apply((String) object); else if (object instanceof Collection) return (T) apply((Collection) object); else if (object instanceof Map) return (T) apply((Map) object); else if (!ReflectionUtils.isPrimitiveOrWrapper(object.getClass())) { final Refl refl = new Refl<>(object); - for (Field field : refl.getNonStaticFields()) { - Object o = refl.getFieldObject(field); - refl.setFieldObject(field, apply(o)); - } + for (Field field : refl.getNonStaticFields()) + ReflectionUtils.setAccessible(field).ifPresent(f -> + refl.setFieldObject(f, apply(f.get(object)))); } return object; } diff --git a/gui/base/src/main/java/it/angrybear/yagl/guis/PageableGUI.java b/gui/base/src/main/java/it/angrybear/yagl/guis/PageableGUI.java index dcfbca886..11165c7ed 100644 --- a/gui/base/src/main/java/it/angrybear/yagl/guis/PageableGUI.java +++ b/gui/base/src/main/java/it/angrybear/yagl/guis/PageableGUI.java @@ -8,7 +8,7 @@ import it.angrybear.yagl.items.Item; import it.angrybear.yagl.viewers.Viewer; import it.fulminazzo.fulmicollection.objects.FieldEquable; -import it.fulminazzo.fulmicollection.structures.Tuple; +import it.fulminazzo.fulmicollection.structures.tuples.Tuple; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/gui/base/src/main/java/it/angrybear/yagl/viewers/Viewer.java b/gui/base/src/main/java/it/angrybear/yagl/viewers/Viewer.java index af91ebb06..be869bee1 100644 --- a/gui/base/src/main/java/it/angrybear/yagl/viewers/Viewer.java +++ b/gui/base/src/main/java/it/angrybear/yagl/viewers/Viewer.java @@ -12,9 +12,21 @@ */ @Getter public abstract class Viewer { + /** + * The Unique id. + */ protected final UUID uniqueId; + /** + * The Name. + */ protected final String name; + /** + * The Previous gui. + */ protected GUI previousGUI; + /** + * The Open gui. + */ protected GUI openGUI; /** @@ -44,6 +56,13 @@ public boolean hasOpenGUI() { */ public abstract void playSound(final @NotNull Sound sound); + /** + * Sends the given message to the player. + * + * @param message the message + */ + public abstract void sendMessage(final @NotNull String message); + /** * Execute command. * diff --git a/gui/base/src/test/java/it/angrybear/yagl/contents/ItemGUIContentTest.java b/gui/base/src/test/java/it/angrybear/yagl/contents/ItemGUIContentTest.java index 8232f18c8..2ab4e5ac2 100644 --- a/gui/base/src/test/java/it/angrybear/yagl/contents/ItemGUIContentTest.java +++ b/gui/base/src/test/java/it/angrybear/yagl/contents/ItemGUIContentTest.java @@ -53,8 +53,8 @@ void testItemMethods() throws InvocationTargetException, IllegalAccessException if (method.getName().equals("isSimilar")) params = new Object[]{expected, new ItemField[0]}; else params = Arrays.stream(method.getParameterTypes()).map(TestUtils::mockParameter).toArray(Object[]::new); - Object obj1 = ReflectionUtils.setAccessible(method).invoke(actual, params); - Object obj2 = ReflectionUtils.setAccessible(method).invoke(expected, params); + Object obj1 = ReflectionUtils.setAccessibleOrThrow(method).invoke(actual, params); + Object obj2 = ReflectionUtils.setAccessibleOrThrow(method).invoke(expected, params); assertEquals(obj2, obj1); } } diff --git a/gui/base/src/test/java/it/angrybear/yagl/guis/PageableGUITest.java b/gui/base/src/test/java/it/angrybear/yagl/guis/PageableGUITest.java index 0fa379c43..65a610f64 100644 --- a/gui/base/src/test/java/it/angrybear/yagl/guis/PageableGUITest.java +++ b/gui/base/src/test/java/it/angrybear/yagl/guis/PageableGUITest.java @@ -9,7 +9,7 @@ import it.angrybear.yagl.viewers.Viewer; import it.angrybear.yagl.wrappers.Sound; import it.fulminazzo.fulmicollection.objects.Refl; -import it.fulminazzo.fulmicollection.structures.Tuple; +import it.fulminazzo.fulmicollection.structures.tuples.Tuple; import it.fulminazzo.fulmicollection.utils.ReflectionUtils; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; @@ -137,7 +137,7 @@ void testPageableGUIMethods() throws InvocationTargetException, IllegalAccessExc Metadatable.class.getDeclaredMethod(method.getName(), method.getParameterTypes()); continue; } catch (NoSuchMethodException ignored) {} - method = ReflectionUtils.setAccessible(method); + method = ReflectionUtils.setAccessibleOrThrow(method); Object[] params = Arrays.stream(method.getParameterTypes()) .map(TestUtils::mockParameter) .map(o -> o instanceof Integer ? 9 : o) @@ -245,6 +245,11 @@ public void playSound(@NotNull Sound sound) { } + @Override + public void sendMessage(@NotNull String message) { + + } + @Override public void executeCommand(@NotNull String command) { diff --git a/gui/bukkit/src/main/java/it/angrybear/yagl/GUIAdapter.java b/gui/bukkit/src/main/java/it/angrybear/yagl/GUIAdapter.java index 8cfca0225..6aeea1dfa 100644 --- a/gui/bukkit/src/main/java/it/angrybear/yagl/GUIAdapter.java +++ b/gui/bukkit/src/main/java/it/angrybear/yagl/GUIAdapter.java @@ -73,12 +73,9 @@ public static void openGUI(final @NotNull GUI gui, @NotNull GUIManager.getOpenGUIViewer(uuid).ifPresent((v, g) -> { reflViewer.setFieldObject("previousGUI", g).setFieldObject("openGUI", null); g.changeGUIAction().ifPresent(a -> a.execute(v, g, gui)); - player.closeInventory(); }); - // Set new GUI - reflViewer.setFieldObject("openGUI", gui); // Open inventory - Inventory inventory = guiToInventory(gui); + Inventory inventory = guiToInventory(gui.apply(gui)); for (int i = 0; i < gui.size(); i++) { GUIContent content = gui.getContent(viewer, i); if (content != null) { @@ -90,6 +87,28 @@ public static void openGUI(final @NotNull GUI gui, @NotNull } } player.openInventory(inventory); + // Set new GUI + reflViewer.setFieldObject("openGUI", gui); + // Execute action if present + gui.openGUIAction().ifPresent(a -> a.execute(reflViewer.getObject(), gui)); + } + + /** + * Closes the currently open {@link GUI} for the specified {@link Viewer}, if present. + * + * @param viewer the viewer + */ + public static void closeGUI(final @NotNull Viewer viewer) { + final UUID uuid = viewer.getUniqueId(); + final Player player = Bukkit.getPlayer(uuid); + if (player == null) throw new PlayerOfflineException(viewer.getName()); + final Refl reflViewer = new Refl<>(viewer); + // Save previous GUI, if present + GUIManager.getOpenGUIViewer(uuid).ifPresent((v, g) -> { + reflViewer.setFieldObject("previousGUI", g).setFieldObject("openGUI", null); + g.closeGUIAction().ifPresent(a -> a.execute(v, g)); + player.closeInventory(); + }); } /** diff --git a/gui/bukkit/src/main/java/it/angrybear/yagl/GUIManager.java b/gui/bukkit/src/main/java/it/angrybear/yagl/GUIManager.java index 73c3957b5..6012ad842 100644 --- a/gui/bukkit/src/main/java/it/angrybear/yagl/GUIManager.java +++ b/gui/bukkit/src/main/java/it/angrybear/yagl/GUIManager.java @@ -4,17 +4,15 @@ import it.angrybear.yagl.guis.GUI; import it.angrybear.yagl.viewers.Viewer; import it.fulminazzo.fulmicollection.objects.Refl; -import it.fulminazzo.fulmicollection.structures.BiOptional; +import it.fulminazzo.fulmicollection.structures.tuples.Tuple; import it.fulminazzo.fulmicollection.utils.ReflectionUtils; import org.bukkit.Bukkit; import org.bukkit.entity.HumanEntity; -import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.inventory.InventoryCloseEvent; import org.bukkit.event.inventory.InventoryDragEvent; -import org.bukkit.event.inventory.InventoryOpenEvent; import org.bukkit.event.server.PluginDisableEvent; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.java.JavaPlugin; @@ -43,12 +41,6 @@ public GUIManager() { this.viewers = new ArrayList<>(); } - @EventHandler - void on(final @NotNull InventoryOpenEvent event) { - getOpenGUIViewer(event.getPlayer()).ifPresent((v, g) -> - g.openGUIAction().ifPresent(a -> a.execute(v, g))); - } - @EventHandler void on(final @NotNull InventoryClickEvent event) { getOpenGUIViewer(event.getWhoClicked()).ifPresent((v, g) -> { @@ -69,9 +61,7 @@ void on(final @NotNull InventoryDragEvent event) { @EventHandler void on(final @NotNull InventoryCloseEvent event) { - Player player = (Player) event.getPlayer(); - getOpenGUIViewer(player).ifPresent((v, g) -> - g.closeGUIAction().ifPresent(a -> a.execute(v, g))); + GUIAdapter.closeGUI(getViewer(event.getPlayer())); } @EventHandler @@ -90,27 +80,27 @@ void on(final @NotNull PluginDisableEvent event) { } /** - * Gets a {@link BiOptional} with the corresponding {@link Viewer} and open {@link GUI} if present. + * Gets a {@link Tuple} with the corresponding {@link Viewer} and open {@link GUI} if present. * * @param player the player - * @return the BiOptional + * @return the Tuple */ - public static @NotNull BiOptional getOpenGUIViewer(final @NotNull HumanEntity player) { + public static @NotNull Tuple getOpenGUIViewer(final @NotNull HumanEntity player) { Viewer viewer = getViewer(player); - if (viewer.hasOpenGUI()) return BiOptional.of(viewer, viewer.getOpenGUI()); - else return BiOptional.empty(); + if (viewer.hasOpenGUI()) return new Tuple<>(viewer, viewer.getOpenGUI()); + else return new Tuple<>(); } /** - * Gets a {@link BiOptional} with the corresponding {@link Viewer} and open {@link GUI} if present. + * Gets a {@link Tuple} with the corresponding {@link Viewer} and open {@link GUI} if present. * * @param uuid the uuid - * @return the BiOptional + * @return the Tuple */ - public static @NotNull BiOptional getOpenGUIViewer(final @NotNull UUID uuid) { + public static @NotNull Tuple getOpenGUIViewer(final @NotNull UUID uuid) { Viewer viewer = getViewer(uuid); - if (viewer != null && viewer.hasOpenGUI()) return BiOptional.of(viewer, viewer.getOpenGUI()); - else return BiOptional.empty(); + if (viewer != null && viewer.hasOpenGUI()) return new Tuple<>(viewer, viewer.getOpenGUI()); + else return new Tuple<>(); } /** diff --git a/gui/bukkit/src/main/java/it/angrybear/yagl/viewers/BukkitViewer.java b/gui/bukkit/src/main/java/it/angrybear/yagl/viewers/BukkitViewer.java index 8ad5290ee..3e2eae9ef 100644 --- a/gui/bukkit/src/main/java/it/angrybear/yagl/viewers/BukkitViewer.java +++ b/gui/bukkit/src/main/java/it/angrybear/yagl/viewers/BukkitViewer.java @@ -32,6 +32,12 @@ public void playSound(@NotNull Sound sound) { WrappersAdapter.playCustomSound(player, sound); } + @Override + public void sendMessage(@NotNull String message) { + Player player = getPlayer().orElseThrow(() -> new PlayerOfflineException(this.name)); + player.sendMessage(message); + } + @Override public void executeCommand(final @NotNull String command) { Player player = getPlayer().orElseThrow(() -> new PlayerOfflineException(this.name)); diff --git a/gui/bukkit/src/test/java/it/angrybear/yagl/GUIAdapterTest.java b/gui/bukkit/src/test/java/it/angrybear/yagl/GUIAdapterTest.java index 19da8003d..654b4c859 100644 --- a/gui/bukkit/src/test/java/it/angrybear/yagl/GUIAdapterTest.java +++ b/gui/bukkit/src/test/java/it/angrybear/yagl/GUIAdapterTest.java @@ -1,5 +1,6 @@ package it.angrybear.yagl; +import it.angrybear.yagl.actions.GUIAction; import it.angrybear.yagl.contents.GUIContent; import it.angrybear.yagl.contents.ItemGUIContent; import it.angrybear.yagl.guis.GUI; @@ -130,8 +131,27 @@ void testOpenGUIForOfflinePlayer() { assertThrowsExactly(PlayerOfflineException.class, () -> openGUI(GUI.newGUI(9))); } + @Test + void testCloseGUIForOfflinePlayer() { + BukkitUtils.removePlayer(this.player); + assertThrowsExactly(PlayerOfflineException.class, this::closeGUI); + } + + @Test + void testOpenGUIAction() { + GUI gui = GUI.newGUI(9); + GUIAction openAction = mock(GUIAction.class); + gui.onOpenGUI(openAction); + openGUI(gui); + verify(openAction).execute(GUIManager.getViewer(this.player), gui); + } + private void openGUI(GUI gui) { GUITestUtils.mockPlugin(p -> gui.open(GUIManager.getViewer(this.player))); } + private void closeGUI() { + GUITestUtils.mockPlugin(p -> GUIAdapter.closeGUI(GUIManager.getViewer(this.player))); + } + } \ No newline at end of file diff --git a/gui/bukkit/src/test/java/it/angrybear/yagl/GUIManagerTest.java b/gui/bukkit/src/test/java/it/angrybear/yagl/GUIManagerTest.java index 2d37fef18..1aee6cdea 100644 --- a/gui/bukkit/src/test/java/it/angrybear/yagl/GUIManagerTest.java +++ b/gui/bukkit/src/test/java/it/angrybear/yagl/GUIManagerTest.java @@ -31,6 +31,14 @@ class GUIManagerTest { + @Test + void testGetOpenGUIViewerPlayer() { + BukkitUtils.setupServer(); + Player player = BukkitUtils.addPlayer(UUID.randomUUID(), "Alex"); + GUITestUtils.mockPlugin(p -> + assertFalse(GUIManager.getOpenGUIViewer(player).isPresent(), "Should not be present")); + } + @Test void testGetOpenGUIViewerUUID() { BukkitUtils.setupServer(); @@ -175,18 +183,6 @@ void testClickOutside() { assertTrue(expected.get(), "Outside action was not invoked"); } - @Test - void testOpenEvent() { - AtomicBoolean expected = new AtomicBoolean(false); - this.expected.onOpenGUI((v, g) -> expected.set(true)); - - InventoryView view = getView(); - - this.guiManager.on(new InventoryOpenEvent(view)); - - assertTrue(expected.get(), "OpenGUI action was not invoked"); - } - @Test void testDragEvent() { InventoryView view = getView(); diff --git a/gui/bukkit/src/test/java/it/angrybear/yagl/viewers/BukkitViewerTest.java b/gui/bukkit/src/test/java/it/angrybear/yagl/viewers/BukkitViewerTest.java index 2a28f8b05..ecd2f86e2 100644 --- a/gui/bukkit/src/test/java/it/angrybear/yagl/viewers/BukkitViewerTest.java +++ b/gui/bukkit/src/test/java/it/angrybear/yagl/viewers/BukkitViewerTest.java @@ -9,7 +9,7 @@ import java.util.UUID; import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; class BukkitViewerTest { private Player player; @@ -19,6 +19,7 @@ class BukkitViewerTest { void setUp() { BukkitUtils.setupServer(); this.player = BukkitUtils.addPlayer(UUID.randomUUID(), "Alex"); + when(this.player.isOnline()).thenReturn(true); this.viewer = BukkitViewer.newViewer(this.player); } @@ -28,6 +29,19 @@ void testNewViewer() { assertEquals(this.player.getName(), this.viewer.getName()); } + @Test + void testSendMessage() { + String expected = "Hello world"; + this.viewer.sendMessage(expected); + verify(this.player, atLeastOnce()).sendMessage(expected); + } + + @Test + void testSendMessageOffline() { + BukkitUtils.removePlayer(this.player); + assertThrowsExactly(PlayerOfflineException.class, () -> this.viewer.sendMessage("Message")); + } + @Test void testPlaySoundOffline() { BukkitUtils.removePlayer(this.player); diff --git a/gui/serializer/src/main/java/it/angrybear/yagl/parsers/PageableGUIParser.java b/gui/serializer/src/main/java/it/angrybear/yagl/parsers/PageableGUIParser.java index d0c852a28..d90ed7dbe 100644 --- a/gui/serializer/src/main/java/it/angrybear/yagl/parsers/PageableGUIParser.java +++ b/gui/serializer/src/main/java/it/angrybear/yagl/parsers/PageableGUIParser.java @@ -7,7 +7,7 @@ import it.fulminazzo.fulmicollection.interfaces.functions.BiFunctionException; import it.fulminazzo.fulmicollection.interfaces.functions.TriConsumer; import it.fulminazzo.fulmicollection.objects.Refl; -import it.fulminazzo.fulmicollection.structures.Tuple; +import it.fulminazzo.fulmicollection.structures.tuples.Tuple; import it.fulminazzo.yamlparser.configuration.ConfigurationSection; import it.fulminazzo.yamlparser.configuration.IConfiguration; import it.fulminazzo.yamlparser.parsers.YAMLParser; diff --git a/gui/serializer/src/test/java/it/angrybear/yagl/parsers/GUIParserTest.java b/gui/serializer/src/test/java/it/angrybear/yagl/parsers/GUIParserTest.java index ace3f0106..6e214156e 100644 --- a/gui/serializer/src/test/java/it/angrybear/yagl/parsers/GUIParserTest.java +++ b/gui/serializer/src/test/java/it/angrybear/yagl/parsers/GUIParserTest.java @@ -88,8 +88,8 @@ void testSaveAndLoadGUI(GUI expected) throws IOException { for (final Field field : new Refl<>(expected).getNonStaticFields()) if (!field.getName().equals("contents")) { - Object obj1 = ReflectionUtils.get(field, expected); - Object obj2 = ReflectionUtils.get(field, actual); + Object obj1 = ReflectionUtils.getOrThrow(field, expected); + Object obj2 = ReflectionUtils.getOrThrow(field, actual); assertEquals(obj1, obj2); } diff --git a/item/base/src/main/java/it/angrybear/yagl/items/ItemImpl.java b/item/base/src/main/java/it/angrybear/yagl/items/ItemImpl.java index 9a256eb5f..c8a464a47 100644 --- a/item/base/src/main/java/it/angrybear/yagl/items/ItemImpl.java +++ b/item/base/src/main/java/it/angrybear/yagl/items/ItemImpl.java @@ -105,8 +105,8 @@ public boolean isSimilar(final @Nullable Item item, final ItemField @NotNull ... .noneMatch(f2 -> f.getName().equalsIgnoreCase(f2.name() .replace("_", "")))) .allMatch(f -> { - Object obj1 = ReflectionUtils.get(f, this); - Object obj2 = ReflectionUtils.get(f, item); + Object obj1 = ReflectionUtils.getOrThrow(f, this); + Object obj2 = ReflectionUtils.getOrThrow(f, item); return Objects.equals(obj1, obj2); }); } diff --git a/item/base/src/test/java/it/angrybear/yagl/items/ItemTest.java b/item/base/src/test/java/it/angrybear/yagl/items/ItemTest.java index b083c0098..80a66a1ef 100644 --- a/item/base/src/test/java/it/angrybear/yagl/items/ItemTest.java +++ b/item/base/src/test/java/it/angrybear/yagl/items/ItemTest.java @@ -79,7 +79,7 @@ void testCopyItemFromAbstractWithNoImpl(Class clazz) { assertTrue(message.contains(implName), "Exception message did not contain abstract implementation class name"); // Check that the returned exception is not from ReflectionUtils - String reflMessage = ReflectionUtils.get(ReflectionUtils.getField(ReflectionUtils.class, "CLASS_NOT_FOUND"), ReflectionUtils.class); + String reflMessage = ReflectionUtils.getOrThrow(ReflectionUtils.getField(ReflectionUtils.class, "CLASS_NOT_FOUND"), ReflectionUtils.class); assertNotEquals(reflMessage.replace("%class%", implName), message, "Exception message should not be the same as the one returned by ReflectionUtils"); } diff --git a/item/bukkit/src/main/java/it/angrybear/yagl/ItemAdapter.java b/item/bukkit/src/main/java/it/angrybear/yagl/ItemAdapter.java index f6e8f44df..a8948a2c8 100644 --- a/item/bukkit/src/main/java/it/angrybear/yagl/ItemAdapter.java +++ b/item/bukkit/src/main/java/it/angrybear/yagl/ItemAdapter.java @@ -9,7 +9,7 @@ import it.angrybear.yagl.items.recipes.ShapelessRecipe; import it.angrybear.yagl.utils.EnumUtils; import it.fulminazzo.fulmicollection.objects.Refl; -import it.fulminazzo.fulmicollection.structures.Tuple; +import it.fulminazzo.fulmicollection.structures.tuples.Tuple; import lombok.AccessLevel; import lombok.NoArgsConstructor; import org.bukkit.Bukkit; @@ -56,8 +56,7 @@ public final class ItemAdapter { invokeNoSuchMethod(() -> item.setUnbreakable(meta.isUnbreakable()), () -> item.setUnbreakable(meta.spigot().isUnbreakable())); invokeNoSuchMethod(() -> { - int modelData = meta.getCustomModelData(); - if (modelData > 0) item.setCustomModelData(modelData); + if (meta.hasCustomModelData()) item.setCustomModelData(meta.getCustomModelData()); }, null); } return item; diff --git a/item/bukkit/src/main/java/it/angrybear/yagl/listeners/PersistentListener.java b/item/bukkit/src/main/java/it/angrybear/yagl/listeners/PersistentListener.java index b68a9988c..046265e1e 100644 --- a/item/bukkit/src/main/java/it/angrybear/yagl/listeners/PersistentListener.java +++ b/item/bukkit/src/main/java/it/angrybear/yagl/listeners/PersistentListener.java @@ -12,6 +12,7 @@ import org.bukkit.event.block.BlockPlaceEvent; import org.bukkit.event.entity.PlayerDeathEvent; import org.bukkit.event.inventory.ClickType; +import org.bukkit.event.inventory.InventoryAction; import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.inventory.InventoryDragEvent; import org.bukkit.event.player.PlayerDropItemEvent; @@ -129,7 +130,7 @@ protected void on(@NotNull InventoryClickEvent event) { ItemStack itemStack = event.getCurrentItem(); Consumer ifPresent = clickConsumer(event, player); - // Check the current item and the cursor; + // Check the current item and the cursor. if (!clickPersistentItem(player, type, ifPresent, itemStack, event.getCursor()) && type.equals(ClickType.NUMBER_KEY)) { // Check if a number has been used from the keyboard to move the item. itemStack = player.getInventory().getItem(event.getHotbarButton()); @@ -174,7 +175,8 @@ protected void on(@NotNull InventoryDragEvent event) { return e -> { Inventory open = player.getOpenInventory().getTopInventory(); int rawSlot = event.getRawSlot(); - if (e.getMobility() != Mobility.INTERNAL || rawSlot < open.getSize()) cancelled(event).accept(e); + if (e.getMobility() != Mobility.INTERNAL || (rawSlot < open.getSize() || event.getAction().equals(InventoryAction.MOVE_TO_OTHER_INVENTORY))) + cancelled(event).accept(e); }; } diff --git a/item/bukkit/src/test/java/it/angrybear/yagl/ItemAdapterTest.java b/item/bukkit/src/test/java/it/angrybear/yagl/ItemAdapterTest.java index b1e47489f..e6ddd4757 100644 --- a/item/bukkit/src/test/java/it/angrybear/yagl/ItemAdapterTest.java +++ b/item/bukkit/src/test/java/it/angrybear/yagl/ItemAdapterTest.java @@ -156,7 +156,7 @@ void testNullRecipe() throws InvocationTargetException, IllegalAccessException { @NotNull List methods = adapter.getMethods(m -> m.getName().equals(methodName)); assertEquals(4, methods.size(), String.format("Could not find all '%s' methods", methodName)); for (Method method : methods) { - Object result = ReflectionUtils.setAccessible(method).invoke(ItemAdapter.class, (Object) null); + Object result = ReflectionUtils.setAccessibleOrThrow(method).invoke(ItemAdapter.class, (Object) null); assertNull(result); } } diff --git a/item/bukkit/src/test/java/it/angrybear/yagl/listeners/PersistentListenerTest.java b/item/bukkit/src/test/java/it/angrybear/yagl/listeners/PersistentListenerTest.java index 160f1f6b3..90e07bd19 100644 --- a/item/bukkit/src/test/java/it/angrybear/yagl/listeners/PersistentListenerTest.java +++ b/item/bukkit/src/test/java/it/angrybear/yagl/listeners/PersistentListenerTest.java @@ -214,7 +214,12 @@ private static InventoryView getInventoryView(Player player, Inventory inventory when(view.getPlayer()).thenReturn(player); when(view.getItem(any(int.class))).thenCallRealMethod(); when(view.getCursor()).thenCallRealMethod(); - when(view.convertSlot(any(int.class))).thenCallRealMethod(); + when(view.convertSlot(any(int.class))).thenAnswer(a -> { + int slot = a.getArgument(0); + int size = inventory.getSize(); + if (slot >= size) slot -= size; + return slot; + }); when(view.getInventory(any(int.class))).thenAnswer(a -> { int slot = a.getArgument(0); if (slot < inventory.getSize()) return inventory; @@ -244,13 +249,14 @@ private InventoryClickEvent[] inventoryClickEvents() { new InventoryClickEvent(view, InventoryType.SlotType.CONTAINER, 0, ClickType.LEFT, InventoryAction.CLONE_STACK), new InventoryClickEvent(view, InventoryType.SlotType.CONTAINER, 2, ClickType.LEFT, InventoryAction.CLONE_STACK), new InventoryClickEvent(view, InventoryType.SlotType.CONTAINER, 3, ClickType.NUMBER_KEY, InventoryAction.CLONE_STACK, 0), + new InventoryClickEvent(view, InventoryType.SlotType.CONTAINER, 9, ClickType.SHIFT_LEFT, InventoryAction.MOVE_TO_OTHER_INVENTORY), }; } @ParameterizedTest @MethodSource("inventoryClickEvents") void simulateInventoryClick(InventoryClickEvent event) { - if (event.getRawSlot() == 2) cursor = maintain.create(); + if (event.getRawSlot() == 2) cursor = maintain.setMobility(Mobility.INTERNAL).create(); assertFalse(clicked, "Clicked should be initialized as false"); assertFalse(event.isCancelled(), "Event should not be cancelled"); diff --git a/item/serializer/src/test/java/it/angrybear/yagl/parsers/RecipeParserTest.java b/item/serializer/src/test/java/it/angrybear/yagl/parsers/RecipeParserTest.java index 8cb548346..57d056f1f 100644 --- a/item/serializer/src/test/java/it/angrybear/yagl/parsers/RecipeParserTest.java +++ b/item/serializer/src/test/java/it/angrybear/yagl/parsers/RecipeParserTest.java @@ -69,8 +69,8 @@ void testRecipe(Recipe recipe) throws IOException { Recipe recipe2 = configuration.get(path, Recipe.class); for (final Field field : recipe.getClass().getDeclaredFields()) { - Object obj1 = ReflectionUtils.get(field, recipe); - Object obj2 = ReflectionUtils.get(field, recipe2); + Object obj1 = ReflectionUtils.getOrThrow(field, recipe); + Object obj2 = ReflectionUtils.getOrThrow(field, recipe2); assertEquals(obj1, obj2); } } diff --git a/settings.gradle b/settings.gradle index c26efb453..4da565f97 100644 --- a/settings.gradle +++ b/settings.gradle @@ -43,6 +43,7 @@ include 'gui:gui-serializer' project(':gui:gui-serializer').projectDir = file('gui/serializer') include 'testing' +include 'demo' gradleEnterprise { if (System.getenv("CI") != null) { @@ -52,4 +53,5 @@ gradleEnterprise { termsOfServiceAgree = 'yes' } } -} \ No newline at end of file +} + diff --git a/testing/src/main/java/it/angrybear/yagl/TestUtils.java b/testing/src/main/java/it/angrybear/yagl/TestUtils.java index 45d9157f1..917c5a520 100644 --- a/testing/src/main/java/it/angrybear/yagl/TestUtils.java +++ b/testing/src/main/java/it/angrybear/yagl/TestUtils.java @@ -85,11 +85,11 @@ public static void testMultipleMethods(final @NotNull Object executor, final @No try { // Execute target method final Object[] parameters = initializeParameters(targetMethod.getParameterTypes(), staticObjects); - ReflectionUtils.setAccessible(targetMethod).invoke(executor, parameters); + ReflectionUtils.setAccessibleOrThrow(targetMethod).invoke(executor, parameters); // Verify execution with mock Method method = target.getClass().getDeclaredMethod(invokedMethod, invokedMethodParamTypes); - ReflectionUtils.setAccessible(method).invoke(verify(target), + ReflectionUtils.setAccessibleOrThrow(method).invoke(verify(target), Arrays.stream(captors).map(ArgumentCaptor::capture).toArray(Object[]::new)); return captors; @@ -169,7 +169,7 @@ public static void testReturnType(final @NotNull T object, final @NotNull Cl methodString, objectClassName); Object[] mockParameters = Arrays.stream(parameters).map(TestUtils::mockParameter).toArray(Object[]::new); - Object o = ReflectionUtils.setAccessible(method).invoke(object, mockParameters); + Object o = ReflectionUtils.setAccessibleOrThrow(method).invoke(object, mockParameters); if (method.getName().equals("copy")) assertInstanceOf(objectClass, o, String.format("Returned object from %s call should have been %s but was %s", diff --git a/wrappers/base/src/main/java/it/angrybear/yagl/particles/DustParticleOption.java b/wrappers/base/src/main/java/it/angrybear/yagl/particles/DustParticleOption.java index 23235d3a1..d9fdb357b 100644 --- a/wrappers/base/src/main/java/it/angrybear/yagl/particles/DustParticleOption.java +++ b/wrappers/base/src/main/java/it/angrybear/yagl/particles/DustParticleOption.java @@ -1,7 +1,7 @@ package it.angrybear.yagl.particles; import it.angrybear.yagl.Color; -import it.fulminazzo.fulmicollection.structures.Tuple; +import it.fulminazzo.fulmicollection.structures.tuples.Tuple; import org.jetbrains.annotations.NotNull; /** diff --git a/wrappers/base/src/main/java/it/angrybear/yagl/particles/DustTransitionParticleOption.java b/wrappers/base/src/main/java/it/angrybear/yagl/particles/DustTransitionParticleOption.java index e6fd7369a..1bbdc7819 100644 --- a/wrappers/base/src/main/java/it/angrybear/yagl/particles/DustTransitionParticleOption.java +++ b/wrappers/base/src/main/java/it/angrybear/yagl/particles/DustTransitionParticleOption.java @@ -1,7 +1,7 @@ package it.angrybear.yagl.particles; import it.angrybear.yagl.Color; -import it.fulminazzo.fulmicollection.structures.Triple; +import it.fulminazzo.fulmicollection.structures.tuples.Triple; import org.jetbrains.annotations.NotNull; /** diff --git a/wrappers/base/src/main/java/it/angrybear/yagl/particles/MaterialDataOption.java b/wrappers/base/src/main/java/it/angrybear/yagl/particles/MaterialDataOption.java index eef94544c..9855e4f95 100644 --- a/wrappers/base/src/main/java/it/angrybear/yagl/particles/MaterialDataOption.java +++ b/wrappers/base/src/main/java/it/angrybear/yagl/particles/MaterialDataOption.java @@ -1,6 +1,6 @@ package it.angrybear.yagl.particles; -import it.fulminazzo.fulmicollection.structures.Tuple; +import it.fulminazzo.fulmicollection.structures.tuples.Tuple; import org.jetbrains.annotations.NotNull; /** diff --git a/wrappers/bukkit/src/main/java/it/angrybear/yagl/WrappersAdapter.java b/wrappers/bukkit/src/main/java/it/angrybear/yagl/WrappersAdapter.java index a441615c4..a7ceee1b8 100644 --- a/wrappers/bukkit/src/main/java/it/angrybear/yagl/WrappersAdapter.java +++ b/wrappers/bukkit/src/main/java/it/angrybear/yagl/WrappersAdapter.java @@ -10,8 +10,8 @@ import it.angrybear.yagl.wrappers.Sound; import it.fulminazzo.fulmicollection.objects.Refl; import it.fulminazzo.fulmicollection.structures.CacheMap; -import it.fulminazzo.fulmicollection.structures.Triple; -import it.fulminazzo.fulmicollection.structures.Tuple; +import it.fulminazzo.fulmicollection.structures.tuples.Triple; +import it.fulminazzo.fulmicollection.structures.tuples.Tuple; import it.fulminazzo.fulmicollection.utils.ReflectionUtils; import lombok.AccessLevel; import lombok.NoArgsConstructor; @@ -425,9 +425,9 @@ else if (dataType.getCanonicalName().equals("org.bukkit.block.data.BlockData")) public static @Nullable ItemStack itemToItemStack(final @Nullable AbstractItem item) { final Class itemUtils; try { - itemUtils = ReflectionUtils.getClass("it.angrybear.yagl.utils.ItemUtils"); + itemUtils = ReflectionUtils.getClass("it.angrybear.yagl.ItemAdapter"); } catch (IllegalArgumentException e) { - throw new IllegalStateException("Could not find ItemUtils class. This function requires the 'item:bukkit' module to be added"); + throw new IllegalStateException("Could not find ItemAdapter class. This function requires the 'item:bukkit' module to be added"); } return new Refl<>(itemUtils).invokeMethod("itemToItemStack", item); } diff --git a/wrappers/bukkit/src/test/java/it/angrybear/yagl/utils/ItemUtils.java b/wrappers/bukkit/src/test/java/it/angrybear/yagl/ItemAdapter.java similarity index 85% rename from wrappers/bukkit/src/test/java/it/angrybear/yagl/utils/ItemUtils.java rename to wrappers/bukkit/src/test/java/it/angrybear/yagl/ItemAdapter.java index 5d1f739a0..b25cba7fd 100644 --- a/wrappers/bukkit/src/test/java/it/angrybear/yagl/utils/ItemUtils.java +++ b/wrappers/bukkit/src/test/java/it/angrybear/yagl/ItemAdapter.java @@ -1,4 +1,4 @@ -package it.angrybear.yagl.utils; +package it.angrybear.yagl; import it.angrybear.yagl.items.AbstractItem; import lombok.AccessLevel; @@ -8,7 +8,7 @@ @SuppressWarnings("deprecation") @NoArgsConstructor(access = AccessLevel.PRIVATE) -public final class ItemUtils { +public final class ItemAdapter { public static ItemStack itemToItemStack(AbstractItem item) { return new ItemStack(Material.STONE, 7); diff --git a/wrappers/bukkit/src/test/java/it/angrybear/yagl/WrappersAdapterTest.java b/wrappers/bukkit/src/test/java/it/angrybear/yagl/WrappersAdapterTest.java index 295b9558d..d0a36133b 100644 --- a/wrappers/bukkit/src/test/java/it/angrybear/yagl/WrappersAdapterTest.java +++ b/wrappers/bukkit/src/test/java/it/angrybear/yagl/WrappersAdapterTest.java @@ -7,7 +7,7 @@ import it.angrybear.yagl.wrappers.PotionEffect; import it.angrybear.yagl.wrappers.Sound; import it.fulminazzo.fulmicollection.objects.Refl; -import it.fulminazzo.fulmicollection.structures.Tuple; +import it.fulminazzo.fulmicollection.structures.tuples.Tuple; import it.fulminazzo.fulmicollection.utils.ReflectionUtils; import it.fulminazzo.jbukkit.BukkitUtils; import it.fulminazzo.jbukkit.annotations.After1_; @@ -287,7 +287,7 @@ private static org.bukkit.potion.PotionEffect[] getPotionEffects() { List potionEffects = new ArrayList<>(); for (Field field : PotionEffectType.class.getDeclaredFields()) if (field.getType().equals(PotionEffectType.class)) { - PotionEffectType type = ReflectionUtils.get(field, PotionEffectType.class); + PotionEffectType type = ReflectionUtils.getOrThrow(field, PotionEffectType.class); potionEffects.add(new MockPotionEffect(type.getId(), field.getName())); } // Register potion effects @@ -310,7 +310,7 @@ private static org.bukkit.enchantments.Enchantment[] getEnchantments() { List enchantments = new ArrayList<>(); for (Field field : org.bukkit.enchantments.Enchantment.class.getDeclaredFields()) if (field.getType().equals(org.bukkit.enchantments.Enchantment.class)) { - org.bukkit.enchantments.Enchantment enchant = ReflectionUtils.get(field, org.bukkit.enchantments.Enchantment.class); + org.bukkit.enchantments.Enchantment enchant = ReflectionUtils.getOrThrow(field, org.bukkit.enchantments.Enchantment.class); enchantments.add(new MockEnchantment(enchant.getKey())); } // Register enchantments diff --git a/wrappers/serializer/src/main/java/it/angrybear/yagl/particles/MaterialDataOptionParser.java b/wrappers/serializer/src/main/java/it/angrybear/yagl/particles/MaterialDataOptionParser.java index 27da299ed..2f149f0c9 100644 --- a/wrappers/serializer/src/main/java/it/angrybear/yagl/particles/MaterialDataOptionParser.java +++ b/wrappers/serializer/src/main/java/it/angrybear/yagl/particles/MaterialDataOptionParser.java @@ -2,7 +2,7 @@ import it.fulminazzo.fulmicollection.interfaces.functions.BiFunctionException; import it.fulminazzo.fulmicollection.interfaces.functions.TriConsumer; -import it.fulminazzo.fulmicollection.structures.Tuple; +import it.fulminazzo.fulmicollection.structures.tuples.Tuple; import it.fulminazzo.yamlparser.configuration.IConfiguration; import it.fulminazzo.yamlparser.parsers.YAMLParser; diff --git a/wrappers/serializer/src/main/java/it/angrybear/yagl/wrappers/WrapperParser.java b/wrappers/serializer/src/main/java/it/angrybear/yagl/wrappers/WrapperParser.java index f39c47d9b..8c2f59e2f 100644 --- a/wrappers/serializer/src/main/java/it/angrybear/yagl/wrappers/WrapperParser.java +++ b/wrappers/serializer/src/main/java/it/angrybear/yagl/wrappers/WrapperParser.java @@ -38,24 +38,35 @@ protected BiFunctionException getLoader() { return (c, s) -> { String raw = c.getString(s); if (raw == null || raw.trim().isEmpty()) return null; - else { - String[] rawData = raw.split(":"); - Constructor constructor = findConstructorFromRaw(rawData); - Object[] parameters = initializeParameters(rawData, constructor); - return new Refl<>(getOClass(), parameters).getObject(); - } + else return parseWrapperFromString(raw, getOClass()); }; } - private @NotNull Constructor findConstructorFromRaw(final String @NotNull [] rawData) throws NoSuchMethodException { - Constructor constructor = (Constructor) Arrays.stream(getOClass().getConstructors()) + /** + * Converts the given string to an instance of the given wrapper class by using the most appropriate constructor. + * + * @param the type of the wrapper + * @param raw the string to convert from + * @param clazz the class of the wrapper + * @return the wrapper + * @throws NoSuchMethodException an exception thrown in case the constructor cannot be found + */ + public static @NotNull W parseWrapperFromString(final @NotNull String raw, final @NotNull Class clazz) throws NoSuchMethodException { + String[] rawData = raw.split(":"); + Constructor constructor = findConstructorFromRaw(rawData, clazz); + Object[] parameters = initializeParameters(rawData, constructor); + return new Refl<>(clazz, parameters).getObject(); + } + + private static @NotNull Constructor findConstructorFromRaw(final String @NotNull [] rawData, final @NotNull Class clazz) throws NoSuchMethodException { + Constructor constructor = (Constructor) Arrays.stream(clazz.getConstructors()) .filter(t -> t.getParameterCount() <= rawData.length) .min(Comparator.comparing(t -> -t.getParameterCount())).orElse(null); if (constructor == null) { StringBuilder builder = new StringBuilder(); for (int i = 0; i < rawData.length; i++) builder.append("?, "); throw new NoSuchMethodException(String.format("Could not find method %s(%s)", - getOClass().getSimpleName(), builder.substring(0, Math.max(0, builder.length() - 2)))); + clazz.getSimpleName(), builder.substring(0, Math.max(0, builder.length() - 2)))); } return constructor; } diff --git a/wrappers/serializer/src/test/java/it/angrybear/yagl/particles/ParticleOptionParserTest.java b/wrappers/serializer/src/test/java/it/angrybear/yagl/particles/ParticleOptionParserTest.java index a890eaf2b..b19348eb8 100644 --- a/wrappers/serializer/src/test/java/it/angrybear/yagl/particles/ParticleOptionParserTest.java +++ b/wrappers/serializer/src/test/java/it/angrybear/yagl/particles/ParticleOptionParserTest.java @@ -59,8 +59,8 @@ void testOptions(ParticleOption expected) throws IOException { Field[] fields = expected.getClass().getDeclaredFields(); for (Field field : fields) { - Object obj1 = ReflectionUtils.get(field, expected); - Object obj2 = ReflectionUtils.get(field, actual); + Object obj1 = ReflectionUtils.getOrThrow(field, expected); + Object obj2 = ReflectionUtils.getOrThrow(field, actual); assertEquals(obj1.getClass(), obj2.getClass()); assertEquals(obj1, obj2); } diff --git a/wrappers/serializer/src/test/java/it/angrybear/yagl/particles/ParticleParserTest.java b/wrappers/serializer/src/test/java/it/angrybear/yagl/particles/ParticleParserTest.java index f4cd942ff..31d5c0a65 100644 --- a/wrappers/serializer/src/test/java/it/angrybear/yagl/particles/ParticleParserTest.java +++ b/wrappers/serializer/src/test/java/it/angrybear/yagl/particles/ParticleParserTest.java @@ -79,8 +79,8 @@ void testTypes(AParticleType type) throws IOException { Field[] fields = expected.getClass().getDeclaredFields(); for (Field field : fields) { - Object obj1 = ReflectionUtils.get(field, expected); - Object obj2 = ReflectionUtils.get(field, actual); + Object obj1 = ReflectionUtils.getOrThrow(field, expected); + Object obj2 = ReflectionUtils.getOrThrow(field, actual); if (obj1 == null) assertNull(obj2); else { assertEquals(obj1.getClass(), obj2.getClass());