diff --git a/.github/workflows/build-master.yml b/.github/workflows/build-master.yml index c21e1810c..e6484e925 100644 --- a/.github/workflows/build-master.yml +++ b/.github/workflows/build-master.yml @@ -59,6 +59,7 @@ jobs: cp -r EssentialsAntiBuild/build/docs/javadoc/ javadocs/EssentialsAntiBuild/ cp -r EssentialsChat/build/docs/javadoc/ javadocs/EssentialsChat/ cp -r EssentialsDiscord/build/docs/javadoc/ javadocs/EssentialsDiscord/ + cp -r EssentialsDiscordLink/build/docs/javadoc/ javadocs/EssentialsDiscordLink/ cp -r EssentialsGeoIP/build/docs/javadoc/ javadocs/EssentialsGeoIP/ cp -r EssentialsProtect/build/docs/javadoc/ javadocs/EssentialsProtect/ cp -r EssentialsSpawn/build/docs/javadoc/ javadocs/EssentialsSpawn/ diff --git a/.idea/checkstyle-idea.xml b/.idea/checkstyle-idea.xml index a118891b7..4b03e0aa6 100644 --- a/.idea/checkstyle-idea.xml +++ b/.idea/checkstyle-idea.xml @@ -24,4 +24,4 @@ - + \ No newline at end of file diff --git a/Essentials/src/main/java/com/earth2me/essentials/EssentialsBlockListener.java b/Essentials/src/main/java/com/earth2me/essentials/EssentialsBlockListener.java index be6ec1fdb..887982033 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/EssentialsBlockListener.java +++ b/Essentials/src/main/java/com/earth2me/essentials/EssentialsBlockListener.java @@ -1,5 +1,6 @@ package com.earth2me.essentials; +import com.earth2me.essentials.craftbukkit.Inventories; import com.earth2me.essentials.utils.MaterialUtil; import net.ess3.api.IEssentials; import net.ess3.provider.PersistentDataProvider; @@ -47,7 +48,7 @@ public class EssentialsBlockListener implements Listener { if (is != null && is.getType() != null && !MaterialUtil.isAir(is.getType())) { final ItemStack cloneIs = is.clone(); cloneIs.setAmount(1); - user.getBase().getInventory().addItem(cloneIs); + Inventories.addItem(user.getBase(), cloneIs); user.getBase().updateInventory(); } }); diff --git a/Essentials/src/main/java/com/earth2me/essentials/EssentialsEntityListener.java b/Essentials/src/main/java/com/earth2me/essentials/EssentialsEntityListener.java index ba305ec0b..a1ab286b9 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/EssentialsEntityListener.java +++ b/Essentials/src/main/java/com/earth2me/essentials/EssentialsEntityListener.java @@ -1,6 +1,6 @@ package com.earth2me.essentials; -import com.earth2me.essentials.utils.MaterialUtil; +import com.earth2me.essentials.craftbukkit.Inventories; import com.earth2me.essentials.utils.VersionUtil; import net.ess3.api.IEssentials; import org.bukkit.Location; @@ -52,12 +52,12 @@ public class EssentialsEntityListener implements Listener { if (eDefend instanceof Player) { onPlayerVsPlayerDamage(event, (Player) eDefend, attacker); } else if (eDefend instanceof Ageable) { - final ItemStack hand = attacker.getBase().getItemInHand(); + final ItemStack hand = Inventories.getItemInMainHand(attacker.getBase()); if (ess.getSettings().isMilkBucketEasterEggEnabled() && hand != null && hand.getType() == Material.MILK_BUCKET) { ((Ageable) eDefend).setBaby(); hand.setType(Material.BUCKET); - attacker.getBase().setItemInHand(hand); + Inventories.setItemInMainHand(attacker.getBase(), hand); attacker.getBase().updateInventory(); event.setCancelled(true); } @@ -98,7 +98,7 @@ public class EssentialsEntityListener implements Listener { } private void onPlayerVsPlayerPowertool(final EntityDamageByEntityEvent event, final Player defender, final User attacker) { - final List commandList = attacker.getPowertool(attacker.getBase().getItemInHand()); + final List commandList = attacker.getPowertool(Inventories.getItemInHand(attacker.getBase())); if (commandList != null && !commandList.isEmpty()) { for (final String tempCommand : commandList) { final String command = powertoolPlayer.matcher(tempCommand).replaceAll(defender.getName()); @@ -196,73 +196,23 @@ public class EssentialsEntityListener implements Listener { final ISettings.KeepInvPolicy vanish = ess.getSettings().getVanishingItemsPolicy(); final ISettings.KeepInvPolicy bind = ess.getSettings().getBindingItemsPolicy(); if (VersionUtil.getServerBukkitVersion().isHigherThanOrEqualTo(VersionUtil.v1_11_2_R01) && (vanish != ISettings.KeepInvPolicy.KEEP || bind != ISettings.KeepInvPolicy.KEEP)) { - for (final ItemStack stack : event.getEntity().getInventory()) { - if (stack != null && !MaterialUtil.isAir(stack.getType())) { - if (stack.getEnchantments().containsKey(Enchantment.VANISHING_CURSE)) { - if (vanish == ISettings.KeepInvPolicy.DELETE) { - event.getEntity().getInventory().remove(stack); - } else if (vanish == ISettings.KeepInvPolicy.DROP) { - event.getDrops().add(stack); - event.getEntity().getInventory().remove(stack); - } - } - if (stack.getEnchantments().containsKey(Enchantment.BINDING_CURSE)) { - if (bind == ISettings.KeepInvPolicy.DELETE) { - event.getEntity().getInventory().remove(stack); - } else if (bind == ISettings.KeepInvPolicy.DROP) { - event.getEntity().getInventory().remove(stack); - event.getDrops().add(stack); - } + Inventories.removeItems(user.getBase(), stack -> { + if (vanish != ISettings.KeepInvPolicy.KEEP && stack.getEnchantments().containsKey(Enchantment.VANISHING_CURSE)) { + if (vanish == ISettings.KeepInvPolicy.DROP) { + event.getDrops().add(stack.clone()); } + return true; } - } - // Now check armor - final ItemStack[] armor = event.getEntity().getInventory().getArmorContents(); - for (int i = 0; i < armor.length; i++) { - final ItemStack stack = armor[i]; - if (stack != null && !MaterialUtil.isAir(stack.getType())) { - if (stack.getEnchantments().containsKey(Enchantment.VANISHING_CURSE)) { - if (vanish == ISettings.KeepInvPolicy.DELETE) { - armor[i] = null; - } else if (vanish == ISettings.KeepInvPolicy.DROP) { - if (!event.getDrops().contains(stack)) { - event.getDrops().add(stack); - } - armor[i] = null; - } - } - if (stack.getEnchantments().containsKey(Enchantment.BINDING_CURSE)) { - if (bind == ISettings.KeepInvPolicy.DELETE) { - armor[i] = null; - } else if (bind == ISettings.KeepInvPolicy.DROP) { - if (!event.getDrops().contains(stack)) { - event.getDrops().add(stack); - } - armor[i] = null; - } + if (bind != ISettings.KeepInvPolicy.KEEP && stack.getEnchantments().containsKey(Enchantment.BINDING_CURSE)) { + if (bind == ISettings.KeepInvPolicy.DROP) { + event.getDrops().add(stack.clone()); } + return true; } - } - event.getEntity().getInventory().setArmorContents(armor); - // Now check offhand - if (VersionUtil.getServerBukkitVersion().isHigherThanOrEqualTo(VersionUtil.v1_9_R01)) { - final ItemStack stack = event.getEntity().getInventory().getItemInOffHand(); - //noinspection ConstantConditions - if (stack != null && !MaterialUtil.isAir(stack.getType())) { - final boolean isVanish = stack.getEnchantments().containsKey(Enchantment.VANISHING_CURSE); - final boolean isBind = stack.getEnchantments().containsKey(Enchantment.BINDING_CURSE); - if (isVanish || isBind) { - event.getEntity().getInventory().setItemInOffHand(null); - if ((isVanish && vanish == ISettings.KeepInvPolicy.DROP) || (isBind && bind == ISettings.KeepInvPolicy.DROP)) { - if (!event.getDrops().contains(stack)) { - event.getDrops().add(stack); - } - } - } - } - } + return false; + }, true); } } } diff --git a/Essentials/src/main/java/com/earth2me/essentials/EssentialsPlayerListener.java b/Essentials/src/main/java/com/earth2me/essentials/EssentialsPlayerListener.java index 2a512210c..fe16bb0b4 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/EssentialsPlayerListener.java +++ b/Essentials/src/main/java/com/earth2me/essentials/EssentialsPlayerListener.java @@ -1,6 +1,7 @@ package com.earth2me.essentials; import com.earth2me.essentials.commands.Commandfireball; +import com.earth2me.essentials.craftbukkit.Inventories; import com.earth2me.essentials.textreader.IText; import com.earth2me.essentials.textreader.KeywordReplacer; import com.earth2me.essentials.textreader.TextInput; @@ -194,17 +195,26 @@ public class EssentialsPlayerListener implements Listener, FakeAccessor { return; } - if (!ess.getSettings().cancelAfkOnMove() && !ess.getSettings().getFreezeAfkPlayers()) { - event.getHandlers().unregister(this); + final User user = ess.getUser(event.getPlayer()); - if (ess.getSettings().isDebug()) { - ess.getLogger().log(Level.INFO, "Unregistering move listener"); + if (user.isFreeze()) { + final Location from = event.getFrom(); + final Location to = event.getTo().clone(); + to.setX(from.getX()); + to.setY(from.getY()); + to.setZ(from.getZ()); + try { + event.setTo(LocationUtil.getSafeDestination(ess, to)); + } catch (final Exception ex) { + event.setTo(to); } - return; } - final User user = ess.getUser(event.getPlayer()); + if (!ess.getSettings().cancelAfkOnMove() && !ess.getSettings().getFreezeAfkPlayers()) { + return; + } + if (user.isAfk() && ess.getSettings().getFreezeAfkPlayers()) { final Location from = event.getFrom(); final Location origTo = event.getTo(); @@ -561,7 +571,7 @@ public class EssentialsPlayerListener implements Listener, FakeAccessor { final User user = ess.getUser(event.getPlayer()); final ItemStack stack = new ItemStack(Material.EGG, 1); if (user.hasUnlimited(stack)) { - user.getBase().getInventory().addItem(stack); + Inventories.addItem(user.getBase(), stack); user.getBase().updateInventory(); } } diff --git a/Essentials/src/main/java/com/earth2me/essentials/IUser.java b/Essentials/src/main/java/com/earth2me/essentials/IUser.java index 3c4e8361f..2625b1fc8 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/IUser.java +++ b/Essentials/src/main/java/com/earth2me/essentials/IUser.java @@ -336,4 +336,8 @@ public interface IUser { List getPastUsernames(); void addPastUsername(String username); + + boolean isFreeze(); + + void setFreeze(boolean freeze); } diff --git a/Essentials/src/main/java/com/earth2me/essentials/Kit.java b/Essentials/src/main/java/com/earth2me/essentials/Kit.java index d6261add8..3e2b0409e 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/Kit.java +++ b/Essentials/src/main/java/com/earth2me/essentials/Kit.java @@ -2,12 +2,11 @@ package com.earth2me.essentials; import com.earth2me.essentials.Trade.OverflowType; import com.earth2me.essentials.commands.NoChargeException; -import com.earth2me.essentials.craftbukkit.InventoryWorkaround; +import com.earth2me.essentials.craftbukkit.Inventories; import com.earth2me.essentials.textreader.IText; import com.earth2me.essentials.textreader.KeywordReplacer; import com.earth2me.essentials.textreader.SimpleTextInput; import com.earth2me.essentials.utils.DateUtil; -import com.earth2me.essentials.utils.MaterialUtil; import com.earth2me.essentials.utils.NumberUtil; import net.ess3.api.IEssentials; import net.ess3.api.events.KitClaimEvent; @@ -15,11 +14,11 @@ import net.ess3.provider.SerializationProvider; import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.PlayerInventory; import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder; import java.math.BigDecimal; import java.util.ArrayList; +import java.util.Arrays; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.List; @@ -215,57 +214,34 @@ public class Kit { stack = metaStack.getItemStack(); } - if (autoEquip) { - final Material material = stack.getType(); - final PlayerInventory inventory = user.getBase().getInventory(); - if (MaterialUtil.isHelmet(material) && isEmptyStack(inventory.getHelmet())) { - inventory.setHelmet(stack); - continue; - } else if (MaterialUtil.isChestplate(material) && isEmptyStack(inventory.getChestplate())) { - inventory.setChestplate(stack); - continue; - } else if (MaterialUtil.isLeggings(material) && isEmptyStack(inventory.getLeggings())) { - inventory.setLeggings(stack); - continue; - } else if (MaterialUtil.isBoots(material) && isEmptyStack(inventory.getBoots())) { - inventory.setBoots(stack); - continue; - } - } - itemList.add(stack); } - final Map overfilled; - final boolean allowOversizedStacks = user.isAuthorized("essentials.oversizedstacks"); + final int maxStackSize = user.isAuthorized("essentials.oversizedstacks") ? ess.getSettings().getOversizedStackSize() : 0; final boolean isDropItemsIfFull = ess.getSettings().isDropItemsIfFull(); - if (isDropItemsIfFull) { - if (allowOversizedStacks) { - overfilled = InventoryWorkaround.addOversizedItems(user.getBase().getInventory(), ess.getSettings().getOversizedStackSize(), itemList.toArray(new ItemStack[0])); - } else { - overfilled = InventoryWorkaround.addItems(user.getBase().getInventory(), itemList.toArray(new ItemStack[0])); + + final ItemStack[] itemArray = itemList.toArray(new ItemStack[0]); + if (!isDropItemsIfFull && !Inventories.hasSpace(user.getBase(), maxStackSize, autoEquip, itemArray)) { + user.sendMessage(tl("kitInvFullNoDrop")); + return false; + } + + final Map leftover = Inventories.addItem(user.getBase(), maxStackSize, autoEquip, itemArray); + if (!isDropItemsIfFull && !leftover.isEmpty()) { + // Inventories#hasSpace should prevent this state from EVER being reached; If it does, something has gone terribly wrong, and we should just give up and hope people report it :( + throw new IllegalStateException("Something has gone terribly wrong while adding items to the user's inventory. Please report this to the EssentialsX developers. Items left over: " + leftover + ". Original items: " + Arrays.toString(itemArray)); + } + + for (final ItemStack itemStack : leftover.values()) { + int spillAmount = itemStack.getAmount(); + if (maxStackSize != 0) { + itemStack.setAmount(Math.min(spillAmount, itemStack.getMaxStackSize())); } - for (final ItemStack itemStack : overfilled.values()) { - int spillAmount = itemStack.getAmount(); - if (!allowOversizedStacks) { - itemStack.setAmount(Math.min(spillAmount, itemStack.getMaxStackSize())); - } - while (spillAmount > 0) { - user.getWorld().dropItemNaturally(user.getLocation(), itemStack); - spillAmount -= itemStack.getAmount(); - } - spew = true; - } - } else { - if (allowOversizedStacks) { - overfilled = InventoryWorkaround.addAllOversizedItems(user.getBase().getInventory(), ess.getSettings().getOversizedStackSize(), itemList.toArray(new ItemStack[0])); - } else { - overfilled = InventoryWorkaround.addAllItems(user.getBase().getInventory(), itemList.toArray(new ItemStack[0])); - } - if (overfilled != null) { - user.sendMessage(tl("kitInvFullNoDrop")); - return false; + while (spillAmount > 0) { + user.getWorld().dropItemNaturally(user.getLocation(), itemStack); + spillAmount -= itemStack.getAmount(); } + spew = true; } user.getBase().updateInventory(); @@ -292,8 +268,4 @@ public class Kit { } return true; } - - private boolean isEmptyStack(ItemStack stack) { - return stack == null || MaterialUtil.isAir(stack.getType()); - } } diff --git a/Essentials/src/main/java/com/earth2me/essentials/MailServiceImpl.java b/Essentials/src/main/java/com/earth2me/essentials/MailServiceImpl.java index a1490e64d..f47ba88fa 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/MailServiceImpl.java +++ b/Essentials/src/main/java/com/earth2me/essentials/MailServiceImpl.java @@ -1,9 +1,11 @@ package com.earth2me.essentials; import net.ess3.api.IUser; -import net.essentialsx.api.v2.services.mail.MailService; +import net.essentialsx.api.v2.events.UserMailEvent; import net.essentialsx.api.v2.services.mail.MailMessage; import net.essentialsx.api.v2.services.mail.MailSender; +import net.essentialsx.api.v2.services.mail.MailService; +import org.bukkit.Bukkit; import org.bukkit.plugin.ServicePriority; import java.text.SimpleDateFormat; @@ -35,6 +37,12 @@ public class MailServiceImpl implements MailService { } private void sendMail(IUser recipient, MailMessage message) { + final UserMailEvent event = new UserMailEvent(recipient, message); + Bukkit.getPluginManager().callEvent(event); + if (event.isCancelled()) { + return; + } + final ArrayList messages = recipient.getMailMessages(); messages.add(0, message); recipient.setMailList(messages); diff --git a/Essentials/src/main/java/com/earth2me/essentials/MobData.java b/Essentials/src/main/java/com/earth2me/essentials/MobData.java index 8a73fd0f2..99a889efc 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/MobData.java +++ b/Essentials/src/main/java/com/earth2me/essentials/MobData.java @@ -1,6 +1,6 @@ package com.earth2me.essentials; -import com.earth2me.essentials.craftbukkit.InventoryWorkaround; +import com.earth2me.essentials.craftbukkit.Inventories; import com.earth2me.essentials.utils.EnumUtil; import com.earth2me.essentials.utils.StringUtil; import com.earth2me.essentials.utils.VersionUtil; @@ -362,8 +362,8 @@ public enum MobData { ((Horse) spawned).getInventory().setArmor(new ItemStack((Material) this.value, 1)); } else if (this.type.equals(EntityType.ZOMBIE.getEntityClass()) || this.type.equals(EntityType.SKELETON)) { final EntityEquipment invent = ((LivingEntity) spawned).getEquipment(); - InventoryWorkaround.setItemInMainHand(invent, new ItemStack((Material) this.value, 1)); - InventoryWorkaround.setItemInMainHandDropChance(invent, 0.1f); + Inventories.setItemInMainHand(invent, new ItemStack((Material) this.value, 1)); + Inventories.setItemInMainHandDropChance(invent, 0.1f); } } else if (this.value.equals(Data.RAID_LEADER)) { ((Raider) spawned).setPatrolLeader(true); diff --git a/Essentials/src/main/java/com/earth2me/essentials/SpawnMob.java b/Essentials/src/main/java/com/earth2me/essentials/SpawnMob.java index a983b016e..57d5432c7 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/SpawnMob.java +++ b/Essentials/src/main/java/com/earth2me/essentials/SpawnMob.java @@ -1,7 +1,7 @@ package com.earth2me.essentials; import com.earth2me.essentials.Mob.MobException; -import com.earth2me.essentials.craftbukkit.InventoryWorkaround; +import com.earth2me.essentials.craftbukkit.Inventories; import com.earth2me.essentials.utils.EnumUtil; import com.earth2me.essentials.utils.LocationUtil; import com.earth2me.essentials.utils.StringUtil; @@ -251,8 +251,8 @@ public final class SpawnMob { private static void defaultMobData(final EntityType type, final Entity spawned) { if (type == EntityType.SKELETON) { final EntityEquipment invent = ((LivingEntity) spawned).getEquipment(); - InventoryWorkaround.setItemInMainHand(invent, new ItemStack(Material.BOW, 1)); - InventoryWorkaround.setItemInMainHandDropChance(invent, 0.1f); + Inventories.setItemInMainHand(invent, new ItemStack(Material.BOW, 1)); + Inventories.setItemInMainHandDropChance(invent, 0.1f); } if (type == MobCompat.ZOMBIFIED_PIGLIN) { @@ -260,8 +260,8 @@ public final class SpawnMob { setVillager(zombie, false); final EntityEquipment invent = zombie.getEquipment(); - InventoryWorkaround.setItemInMainHand(invent, new ItemStack(GOLDEN_SWORD, 1)); - InventoryWorkaround.setItemInMainHandDropChance(invent, 0.1f); + Inventories.setItemInMainHand(invent, new ItemStack(GOLDEN_SWORD, 1)); + Inventories.setItemInMainHandDropChance(invent, 0.1f); } if (type == EntityType.ZOMBIE) { diff --git a/Essentials/src/main/java/com/earth2me/essentials/Trade.java b/Essentials/src/main/java/com/earth2me/essentials/Trade.java index 7d241658c..c82bf51cb 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/Trade.java +++ b/Essentials/src/main/java/com/earth2me/essentials/Trade.java @@ -1,6 +1,6 @@ package com.earth2me.essentials; -import com.earth2me.essentials.craftbukkit.InventoryWorkaround; +import com.earth2me.essentials.craftbukkit.Inventories; import com.earth2me.essentials.craftbukkit.SetExpFix; import com.earth2me.essentials.utils.NumberUtil; import com.earth2me.essentials.utils.VersionUtil; @@ -8,7 +8,6 @@ import net.ess3.api.IEssentials; import net.ess3.api.IUser; import net.ess3.api.MaxMoneyException; import org.bukkit.Location; -import org.bukkit.entity.Item; import org.bukkit.inventory.ItemStack; import java.io.File; @@ -16,6 +15,7 @@ import java.io.FileWriter; import java.io.IOException; import java.math.BigDecimal; import java.text.DateFormat; +import java.util.Collections; import java.util.Date; import java.util.Locale; import java.util.Map; @@ -197,7 +197,7 @@ public class Trade { return; } - if (getItemStack() != null && !user.getBase().getInventory().containsAtLeast(itemStack, itemStack.getAmount())) { + if (getItemStack() != null && !Inventories.containsAtLeast(user.getBase(), itemStack, itemStack.getAmount())) { future.completeExceptionally(new ChargeException(tl("missingItems", getItemStack().getAmount(), ess.getItemDb().name(getItemStack())))); return; } @@ -225,52 +225,33 @@ public class Trade { user.giveMoney(getMoney()); } if (getItemStack() != null) { - // This stores the would be overflow - final Map overFlow = InventoryWorkaround.addAllItems(user.getBase().getInventory(), getItemStack()); + if (type == OverflowType.ABORT && !Inventories.hasSpace(user.getBase(), 0, false, getItemStack())) { + if (ess.getSettings().isDebug()) { + ess.getLogger().log(Level.INFO, "abort paying " + user.getName() + " itemstack " + getItemStack().toString() + " due to lack of inventory space "); + } + return Collections.singletonMap(0, getItemStack()); + } - if (overFlow != null) { - switch (type) { - case ABORT: - if (ess.getSettings().isDebug()) { - ess.getLogger().log(Level.INFO, "abort paying " + user.getName() + " itemstack " + getItemStack().toString() + " due to lack of inventory space "); + final Map leftover = Inventories.addItem(user.getBase(), getItemStack()); + user.getBase().updateInventory(); + if (!leftover.isEmpty()) { + if (type == OverflowType.RETURN) { + if (ess.getSettings().isDebug()) { + ess.getLogger().log(Level.INFO, "paying " + user.getName() + " partial itemstack " + getItemStack().toString() + " with overflow " + leftover.get(0).toString()); + } + return leftover; + } else { + for (final ItemStack itemStack : leftover.values()) { + int spillAmount = itemStack.getAmount(); + itemStack.setAmount(Math.min(spillAmount, itemStack.getMaxStackSize())); + while (spillAmount > 0) { + user.getBase().getWorld().dropItemNaturally(user.getBase().getLocation(), itemStack); + spillAmount -= itemStack.getAmount(); } - - return overFlow; - - case RETURN: - // Pay the user the items, and return overflow - final Map returnStack = InventoryWorkaround.addItems(user.getBase().getInventory(), getItemStack()); - user.getBase().updateInventory(); - - if (ess.getSettings().isDebug()) { - ess.getLogger().log(Level.INFO, "paying " + user.getName() + " partial itemstack " + getItemStack().toString() + " with overflow " + returnStack.get(0).toString()); - } - - return returnStack; - case DROP: - // Pay the users the items directly, and drop the rest, will always return no overflow. - final Map leftOver = InventoryWorkaround.addItems(user.getBase().getInventory(), getItemStack()); - final Location loc = user.getBase().getLocation(); - for (final ItemStack loStack : leftOver.values()) { - final int maxStackSize = loStack.getType().getMaxStackSize(); - final int stacks = loStack.getAmount() / maxStackSize; - final int leftover = loStack.getAmount() % maxStackSize; - final Item[] itemStacks = new Item[stacks + (leftover > 0 ? 1 : 0)]; - for (int i = 0; i < stacks; i++) { - final ItemStack stack = loStack.clone(); - stack.setAmount(maxStackSize); - itemStacks[i] = loc.getWorld().dropItem(loc, stack); - } - if (leftover > 0) { - final ItemStack stack = loStack.clone(); - stack.setAmount(leftover); - itemStacks[stacks] = loc.getWorld().dropItem(loc, stack); - } - } - if (ess.getSettings().isDebug()) { - ess.getLogger().log(Level.INFO, "paying " + user.getName() + " partial itemstack " + getItemStack().toString() + " and dropping overflow " + leftOver.get(0).toString()); - } - break; + } + if (ess.getSettings().isDebug()) { + ess.getLogger().log(Level.INFO, "paying " + user.getName() + " partial itemstack " + getItemStack().toString() + " and dropping overflow " + leftover.get(0).toString()); + } } } else if (ess.getSettings().isDebug()) { ess.getLogger().log(Level.INFO, "paying " + user.getName() + " itemstack " + getItemStack().toString()); @@ -315,11 +296,11 @@ public class Trade { if (ess.getSettings().isDebug()) { ess.getLogger().log(Level.INFO, "charging user " + user.getName() + " itemstack " + getItemStack().toString()); } - if (!user.getBase().getInventory().containsAtLeast(getItemStack(), getItemStack().getAmount())) { + if (!Inventories.containsAtLeast(user.getBase(), getItemStack(), getItemStack().getAmount())) { future.completeExceptionally(new ChargeException(tl("missingItems", getItemStack().getAmount(), getItemStack().getType().toString().toLowerCase(Locale.ENGLISH).replace("_", " ")))); return; } - user.getBase().getInventory().removeItem(getItemStack()); + Inventories.removeItemAmount(user.getBase(), getItemStack(), getItemStack().getAmount()); user.getBase().updateInventory(); } if (command != null) { diff --git a/Essentials/src/main/java/com/earth2me/essentials/User.java b/Essentials/src/main/java/com/earth2me/essentials/User.java index fd1d9f168..4c63fd0be 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/User.java +++ b/Essentials/src/main/java/com/earth2me/essentials/User.java @@ -1,6 +1,7 @@ package com.earth2me.essentials; import com.earth2me.essentials.commands.IEssentialsCommand; +import com.earth2me.essentials.craftbukkit.Inventories; import com.earth2me.essentials.economy.EconomyLayer; import com.earth2me.essentials.economy.EconomyLayers; import com.earth2me.essentials.messaging.IMessageRecipient; @@ -27,7 +28,6 @@ import org.bukkit.block.Block; import org.bukkit.entity.Player; import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause; import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.PlayerInventory; import org.bukkit.metadata.FixedMetadataValue; import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; @@ -90,6 +90,7 @@ public class User extends UserData implements Comparable, IMessageRecipien private String lastHomeConfirmation; private long lastHomeConfirmationTimestamp; private Boolean toggleShout; + private boolean freeze = false; private transient final List signCopy = Lists.newArrayList("", "", "", ""); private transient long lastVanishTime = System.currentTimeMillis(); @@ -1141,12 +1142,7 @@ public class User extends UserData implements Comparable, IMessageRecipien * Returns the {@link ItemStack} in the main hand or off-hand. If the main hand is empty then the offhand item is returned - also nullable. */ public ItemStack getItemInHand() { - if (VersionUtil.getServerBukkitVersion().isLowerThan(VersionUtil.v1_9_R01)) { - return getBase().getInventory().getItemInHand(); - } else { - final PlayerInventory inventory = getBase().getInventory(); - return inventory.getItemInMainHand() != null ? inventory.getItemInMainHand() : inventory.getItemInOffHand(); - } + return Inventories.getItemInHand(getBase()); } @Override @@ -1196,6 +1192,16 @@ public class User extends UserData implements Comparable, IMessageRecipien return signCopy; } + @Override + public boolean isFreeze() { + return freeze; + } + + @Override + public void setFreeze(boolean freeze) { + this.freeze = freeze; + } + public boolean isBaltopExempt() { if (getBase().isOnline()) { final boolean exempt = isAuthorized("essentials.balancetop.exclude"); diff --git a/Essentials/src/main/java/com/earth2me/essentials/Worth.java b/Essentials/src/main/java/com/earth2me/essentials/Worth.java index 1e8ca1107..5b48841f5 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/Worth.java +++ b/Essentials/src/main/java/com/earth2me/essentials/Worth.java @@ -3,6 +3,7 @@ package com.earth2me.essentials; import com.earth2me.essentials.commands.NotEnoughArgumentsException; import com.earth2me.essentials.config.ConfigurateUtil; import com.earth2me.essentials.config.EssentialsConfiguration; +import com.earth2me.essentials.craftbukkit.Inventories; import com.earth2me.essentials.utils.VersionUtil; import org.bukkit.Material; import org.bukkit.inventory.ItemStack; @@ -101,7 +102,7 @@ public class Worth implements IConf { } int max = 0; - for (final ItemStack s : user.getBase().getInventory().getContents()) { + for (final ItemStack s : Inventories.getInventory(user.getBase(), false)) { if (s == null || !s.isSimilar(is)) { continue; } diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandbook.java b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandbook.java index c7acdca5b..bec93ba1c 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandbook.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandbook.java @@ -2,7 +2,7 @@ package com.earth2me.essentials.commands; import com.earth2me.essentials.User; import com.earth2me.essentials.utils.FormatUtil; -import com.earth2me.essentials.craftbukkit.InventoryWorkaround; +import com.earth2me.essentials.craftbukkit.Inventories; import com.earth2me.essentials.utils.EnumUtil; import com.google.common.collect.Lists; import org.bukkit.Material; @@ -52,7 +52,7 @@ public class Commandbook extends EssentialsCommand { if (isAuthor(bmeta, player) || user.isAuthorized("essentials.book.others")) { final ItemStack newItem = new ItemStack(WRITABLE_BOOK, item.getAmount()); newItem.setItemMeta(bmeta); - InventoryWorkaround.setItemInMainHand(user.getBase(), newItem); + Inventories.setItemInMainHand(user.getBase(), newItem); user.sendMessage(tl("editBookContents")); } else { throw new Exception(tl("denyBookEdit")); @@ -65,7 +65,7 @@ public class Commandbook extends EssentialsCommand { } final ItemStack newItem = new ItemStack(Material.WRITTEN_BOOK, item.getAmount()); newItem.setItemMeta(bmeta); - InventoryWorkaround.setItemInMainHand(user.getBase(), newItem); + Inventories.setItemInMainHand(user.getBase(), newItem); user.sendMessage(tl("bookLocked")); } else { throw new Exception(tl("holdBook")); diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandclearinventory.java b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandclearinventory.java index 2f98a21bd..08b01ae24 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandclearinventory.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandclearinventory.java @@ -2,7 +2,7 @@ package com.earth2me.essentials.commands; import com.earth2me.essentials.CommandSource; import com.earth2me.essentials.User; -import com.earth2me.essentials.craftbukkit.InventoryWorkaround; +import com.earth2me.essentials.craftbukkit.Inventories; import com.earth2me.essentials.utils.NumberUtil; import com.earth2me.essentials.utils.StringUtil; import com.earth2me.essentials.utils.VersionUtil; @@ -22,8 +22,6 @@ import java.util.Set; import static com.earth2me.essentials.I18n.tl; public class Commandclearinventory extends EssentialsCommand { - - private static final int BASE_AMOUNT = 100000; private static final int EXTENDED_CAP = 8; public Commandclearinventory() { @@ -114,38 +112,30 @@ public class Commandclearinventory extends EssentialsCommand { } } - if (type == ClearHandlerType.ALL_EXCEPT_ARMOR) { + if (type != ClearHandlerType.SPECIFIC_ITEM) { + final boolean armor = type == ClearHandlerType.ALL_INCLUDING_ARMOR; if (showExtended) { - sender.sendMessage(tl("inventoryClearingAllItems", player.getDisplayName())); + sender.sendMessage(tl(armor ? "inventoryClearingAllArmor" : "inventoryClearingAllItems", player.getDisplayName())); } - InventoryWorkaround.clearInventoryNoArmor(player.getInventory()); - InventoryWorkaround.setItemInOffHand(player, null); - } else if (type == ClearHandlerType.ALL_INCLUDING_ARMOR) { - if (showExtended) { - sender.sendMessage(tl("inventoryClearingAllArmor", player.getDisplayName())); - } - InventoryWorkaround.clearInventoryNoArmor(player.getInventory()); - InventoryWorkaround.setItemInOffHand(player, null); - player.getInventory().setArmorContents(null); + Inventories.removeItems(player, item -> true, armor); } else { for (final Item item : items) { final ItemStack stack = new ItemStack(item.getMaterial()); if (VersionUtil.PRE_FLATTENING) { + //noinspection deprecation stack.setDurability(item.getData()); } + // amount -1 means all items will be cleared if (amount == -1) { - stack.setAmount(BASE_AMOUNT); - final ItemStack removedStack = player.getInventory().removeItem(stack).get(0); - final int removedAmount = BASE_AMOUNT - removedStack.getAmount() + InventoryWorkaround.clearItemInOffHand(player, stack); + final int removedAmount = Inventories.removeItemSimilar(player, stack, true); if (removedAmount > 0 || showExtended) { sender.sendMessage(tl("inventoryClearingStack", removedAmount, stack.getType().toString().toLowerCase(Locale.ENGLISH), player.getDisplayName())); } } else { stack.setAmount(amount < 0 ? 1 : amount); - if (player.getInventory().containsAtLeast(stack, amount)) { + if (Inventories.removeItemAmount(player, stack, amount)) { sender.sendMessage(tl("inventoryClearingStack", amount, stack.getType().toString().toLowerCase(Locale.ENGLISH), player.getDisplayName())); - player.getInventory().removeItem(stack); } else { if (showExtended) { sender.sendMessage(tl("inventoryClearFail", player.getDisplayName(), amount, stack.getType().toString().toLowerCase(Locale.ENGLISH))); @@ -203,7 +193,7 @@ public class Commandclearinventory extends EssentialsCommand { } private String formatCommand(final String commandLabel, final String[] args) { - return "/" + commandLabel + " " + StringUtil.joinList(" ", args); + return "/" + commandLabel + " " + StringUtil.joinList(" ", (Object[]) args); } private enum ClearHandlerType { diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandcondense.java b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandcondense.java index d6c869e47..c17389983 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandcondense.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandcondense.java @@ -4,6 +4,7 @@ import com.earth2me.essentials.ChargeException; import com.earth2me.essentials.Trade; import com.earth2me.essentials.Trade.OverflowType; import com.earth2me.essentials.User; +import com.earth2me.essentials.craftbukkit.Inventories; import com.earth2me.essentials.utils.VersionUtil; import net.ess3.api.MaxMoneyException; import org.bukkit.Material; @@ -39,7 +40,7 @@ public class Commandcondense extends EssentialsCommand { if (args.length > 0) { is = ess.getItemDb().getMatching(user, args); } else { - for (final ItemStack stack : user.getBase().getInventory().getContents()) { + for (final ItemStack stack : Inventories.getInventory(user.getBase(), false)) { if (stack == null || stack.getType() == Material.AIR) { continue; } @@ -85,7 +86,7 @@ public class Commandcondense extends EssentialsCommand { int amount = 0; - for (final ItemStack contents : user.getBase().getInventory().getContents()) { + for (final ItemStack contents : Inventories.getInventory(user.getBase(), false)) { if (contents != null && contents.isSimilar(stack)) { amount += contents.getAmount(); } diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandcreatekit.java b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandcreatekit.java index 9272749dd..29ae3a853 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandcreatekit.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandcreatekit.java @@ -2,6 +2,7 @@ package com.earth2me.essentials.commands; import com.earth2me.essentials.CommandSource; import com.earth2me.essentials.User; +import com.earth2me.essentials.craftbukkit.Inventories; import com.earth2me.essentials.utils.DateUtil; import com.earth2me.essentials.utils.PasteUtil; import net.ess3.provider.SerializationProvider; @@ -38,7 +39,7 @@ public class Commandcreatekit extends EssentialsCommand { // Command handler will auto fail if this fails. final long delay = Long.parseLong(args[1]); final String kitname = args[0]; - final ItemStack[] items = user.getBase().getInventory().getContents(); + final ItemStack[] items = Inventories.getInventory(user.getBase(), true); final List list = new ArrayList<>(); boolean useSerializationProvider = ess.getSettings().isUseBetterKits(); diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandenchant.java b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandenchant.java index c5fbe25ca..4a6292f18 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandenchant.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandenchant.java @@ -3,7 +3,7 @@ package com.earth2me.essentials.commands; import com.earth2me.essentials.Enchantments; import com.earth2me.essentials.MetaItemStack; import com.earth2me.essentials.User; -import com.earth2me.essentials.craftbukkit.InventoryWorkaround; +import com.earth2me.essentials.craftbukkit.Inventories; import com.earth2me.essentials.utils.StringUtil; import com.google.common.collect.Lists; import org.bukkit.Material; @@ -57,7 +57,7 @@ public class Commandenchant extends EssentialsCommand { final MetaItemStack metaStack = new MetaItemStack(stack); final Enchantment enchantment = metaStack.getEnchantment(user, args[0]); metaStack.addEnchantment(user.getSource(), ess.getSettings().allowUnsafeEnchantments() && user.isAuthorized("essentials.enchantments.allowunsafe"), enchantment, level); - InventoryWorkaround.setItemInMainHand(user.getBase(), metaStack.getItemStack()); + Inventories.setItemInMainHand(user.getBase(), metaStack.getItemStack()); user.getBase().updateInventory(); final String enchantName = enchantment.getName().toLowerCase(Locale.ENGLISH).replace('_', ' '); if (level == 0) { diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandessentials.java b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandessentials.java index dee3bdfd7..cecc44932 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandessentials.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandessentials.java @@ -3,6 +3,7 @@ package com.earth2me.essentials.commands; import com.earth2me.essentials.CommandSource; import com.earth2me.essentials.EssentialsUpgrade; import com.earth2me.essentials.User; +import com.earth2me.essentials.craftbukkit.Inventories; import com.earth2me.essentials.economy.EconomyLayer; import com.earth2me.essentials.economy.EconomyLayers; import com.earth2me.essentials.userstorage.ModernUserMap; @@ -24,11 +25,13 @@ import net.ess3.provider.OnlineModeProvider; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.Location; +import org.bukkit.Material; import org.bukkit.Server; import org.bukkit.Sound; import org.bukkit.World; import org.bukkit.command.Command; import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.PluginDescriptionFile; import org.bukkit.plugin.PluginManager; @@ -95,6 +98,7 @@ public class Commandessentials extends EssentialsCommand { "EssentialsAntiBuild", "EssentialsChat", "EssentialsDiscord", + "EssentialsDiscordLink", "EssentialsGeoIP", "EssentialsProtect", "EssentialsSpawn", @@ -156,6 +160,10 @@ public class Commandessentials extends EssentialsCommand { runUserMap(sender, args); break; + case "itemtest": + runItemTest(server, sender, commandLabel, args); + break; + // "#EasterEgg" case "nya": case "nyan": @@ -170,6 +178,56 @@ public class Commandessentials extends EssentialsCommand { } } + public void runItemTest(Server server, CommandSource sender, String commandLabel, String[] args) { + if (!sender.isAuthorized("essentials.itemtest", ess) || args.length < 2 || !sender.isPlayer()) { + return; + } + + final Player player = sender.getPlayer(); + assert player != null; + + switch (args[1]) { + case "slot": { + if (args.length < 3) { + return; + } + player.getInventory().setItem(Integer.parseInt(args[2]), new ItemStack(Material.DIRT)); + break; + } + case "overfill": { + sender.sendMessage(Inventories.addItem(player, 42, false, new ItemStack(Material.DIAMOND_SWORD, 1), new ItemStack(Material.DIRT, 32), new ItemStack(Material.DIRT, 32)).toString()); + break; + } + case "overfill2": { + if (args.length < 4) { + return; + } + final boolean armor = Boolean.parseBoolean(args[2]); + final boolean add = Boolean.parseBoolean(args[3]); + final ItemStack[] items = new ItemStack[]{new ItemStack(Material.DIAMOND_SWORD, 1), new ItemStack(Material.DIRT, 32), new ItemStack(Material.DIRT, 32), new ItemStack(Material.DIAMOND_HELMET, 4), new ItemStack(Material.CHAINMAIL_LEGGINGS, 1)}; + if (Inventories.hasSpace(player, 0, armor, items)) { + if (add) { + sender.sendMessage(Inventories.addItem(player, 0, armor, items).toString()); + } + sender.sendMessage("SO MUCH SPACE!"); + } else { + sender.sendMessage("No space!"); + } + break; + } + case "remove": { + if (args.length < 3) { + return; + } + Inventories.removeItemExact(player, new ItemStack(Material.PUMPKIN, 1), Boolean.parseBoolean(args[2])); + break; + } + default: { + break; + } + } + } + // Displays the command's usage. private void showUsage(final CommandSource sender) throws Exception { throw new NotEnoughArgumentsException(); diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandgive.java b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandgive.java index 981bd7cc8..f1fc18a31 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandgive.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandgive.java @@ -3,7 +3,7 @@ package com.earth2me.essentials.commands; import com.earth2me.essentials.CommandSource; import com.earth2me.essentials.MetaItemStack; import com.earth2me.essentials.User; -import com.earth2me.essentials.craftbukkit.InventoryWorkaround; +import com.earth2me.essentials.craftbukkit.Inventories; import com.earth2me.essentials.utils.NumberUtil; import com.earth2me.essentials.utils.VersionUtil; import com.google.common.collect.Lists; @@ -81,13 +81,8 @@ public class Commandgive extends EssentialsLoopCommand { final ItemStack finalStack = stack; loopOnlinePlayersConsumer(server, sender, false, true, args[0], player -> { sender.sendMessage(tl("giveSpawn", finalStack.getAmount(), itemName, player.getDisplayName())); - final Map leftovers; - if (player.isAuthorized("essentials.oversizedstacks")) { - leftovers = InventoryWorkaround.addOversizedItems(player.getBase().getInventory(), ess.getSettings().getOversizedStackSize(), finalStack); - } else { - leftovers = InventoryWorkaround.addItems(player.getBase().getInventory(), finalStack); - } + final Map leftovers = Inventories.addItem(player.getBase(), player.isAuthorized("essentials.oversizedstacks") ? ess.getSettings().getOversizedStackSize() : 0, finalStack); for (final ItemStack item : leftovers.values()) { if (isDropItemsIfFull) { diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandhat.java b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandhat.java index b24ea3e6a..2dfc328f9 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandhat.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandhat.java @@ -1,7 +1,7 @@ package com.earth2me.essentials.commands; import com.earth2me.essentials.User; -import com.earth2me.essentials.craftbukkit.InventoryWorkaround; +import com.earth2me.essentials.craftbukkit.Inventories; import com.earth2me.essentials.utils.TriState; import com.earth2me.essentials.utils.VersionUtil; import com.google.common.collect.Lists; @@ -67,7 +67,7 @@ public class Commandhat extends EssentialsCommand { } else { final ItemStack air = new ItemStack(Material.AIR); inv.setHelmet(air); - InventoryWorkaround.addItems(user.getBase().getInventory(), head); + Inventories.addItem(user.getBase(), head); user.sendMessage(tl("hatRemoved")); } } diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/Commanditem.java b/Essentials/src/main/java/com/earth2me/essentials/commands/Commanditem.java index 8eccbe43b..a8524dcbb 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/Commanditem.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/Commanditem.java @@ -2,7 +2,7 @@ package com.earth2me.essentials.commands; import com.earth2me.essentials.MetaItemStack; import com.earth2me.essentials.User; -import com.earth2me.essentials.craftbukkit.InventoryWorkaround; +import com.earth2me.essentials.craftbukkit.Inventories; import com.google.common.collect.Lists; import org.bukkit.Material; import org.bukkit.Server; @@ -63,11 +63,7 @@ public class Commanditem extends EssentialsCommand { final String displayName = stack.getType().toString().toLowerCase(Locale.ENGLISH).replace('_', ' '); user.sendMessage(tl("itemSpawn", stack.getAmount(), displayName)); - if (user.isAuthorized("essentials.oversizedstacks")) { - InventoryWorkaround.addOversizedItems(user.getBase().getInventory(), ess.getSettings().getOversizedStackSize(), stack); - } else { - InventoryWorkaround.addItems(user.getBase().getInventory(), stack); - } + Inventories.addItem(user.getBase(), user.isAuthorized("essentials.oversizedstacks") ? ess.getSettings().getOversizedStackSize() : 0, stack); user.getBase().updateInventory(); } diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/Commanditemlore.java b/Essentials/src/main/java/com/earth2me/essentials/commands/Commanditemlore.java index 3ee0a4eb8..9f605023d 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/Commanditemlore.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/Commanditemlore.java @@ -1,6 +1,7 @@ package com.earth2me.essentials.commands; import com.earth2me.essentials.User; +import com.earth2me.essentials.craftbukkit.Inventories; import com.earth2me.essentials.utils.FormatUtil; import com.earth2me.essentials.utils.MaterialUtil; import com.earth2me.essentials.utils.NumberUtil; @@ -24,8 +25,8 @@ public class Commanditemlore extends EssentialsCommand { @Override protected void run(final Server server, final User user, final String commandLabel, final String[] args) throws Exception { - final ItemStack item = user.getBase().getItemInHand(); - if (MaterialUtil.isAir(item.getType())) { + final ItemStack item = Inventories.getItemInHand(user.getBase()); + if (item == null || MaterialUtil.isAir(item.getType())) { throw new Exception(tl("itemloreInvalidItem")); } @@ -73,8 +74,8 @@ public class Commanditemlore extends EssentialsCommand { } else if (args.length == 2) { switch (args[0].toLowerCase(Locale.ENGLISH)) { case "set": { - final ItemStack item = user.getBase().getItemInHand(); - if (!MaterialUtil.isAir(item.getType()) && item.hasItemMeta() && item.getItemMeta().hasLore()) { + final ItemStack item = Inventories.getItemInHand(user.getBase()); + if (item != null && !MaterialUtil.isAir(item.getType()) && item.hasItemMeta() && item.getItemMeta().hasLore()) { final List lineNumbers = new ArrayList<>(); for (int i = 1; i <= item.getItemMeta().getLore().size(); i++) { lineNumbers.add(String.valueOf(i)); @@ -92,8 +93,8 @@ public class Commanditemlore extends EssentialsCommand { } else if (args.length == 3) { if (args[0].equalsIgnoreCase("set") && NumberUtil.isInt(args[1])) { final int i = Integer.parseInt(args[1]); - final ItemStack item = user.getBase().getItemInHand(); - if (!MaterialUtil.isAir(item.getType()) && item.hasItemMeta() && item.getItemMeta().hasLore() && item.getItemMeta().getLore().size() >= i) { + final ItemStack item = Inventories.getItemInHand(user.getBase()); + if (item != null && !MaterialUtil.isAir(item.getType()) && item.hasItemMeta() && item.getItemMeta().hasLore() && item.getItemMeta().getLore().size() >= i) { return Lists.newArrayList(FormatUtil.unformatString(user, "essentials.itemlore", item.getItemMeta().getLore().get(i - 1))); } } diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/Commanditemname.java b/Essentials/src/main/java/com/earth2me/essentials/commands/Commanditemname.java index d2fd65960..b4666b86a 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/Commanditemname.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/Commanditemname.java @@ -1,6 +1,7 @@ package com.earth2me.essentials.commands; import com.earth2me.essentials.User; +import com.earth2me.essentials.craftbukkit.Inventories; import com.earth2me.essentials.utils.FormatUtil; import com.earth2me.essentials.utils.MaterialUtil; import com.earth2me.essentials.utils.TriState; @@ -23,8 +24,8 @@ public class Commanditemname extends EssentialsCommand { @Override protected void run(final Server server, final User user, final String commandLabel, final String[] args) throws Exception { - final ItemStack item = user.getBase().getItemInHand(); - if (MaterialUtil.isAir(item.getType())) { + final ItemStack item = Inventories.getItemInHand(user.getBase()); + if (item == null || MaterialUtil.isAir(item.getType())) { user.sendMessage(tl("itemnameInvalidItem")); return; } @@ -50,8 +51,8 @@ public class Commanditemname extends EssentialsCommand { @Override protected List getTabCompleteOptions(Server server, User user, String commandLabel, String[] args) { if (args.length == 1) { - final ItemStack item = user.getBase().getItemInHand(); - if (!MaterialUtil.isAir(item.getType()) && item.hasItemMeta() && item.getItemMeta().hasDisplayName()) { + final ItemStack item = Inventories.getItemInHand(user.getBase()); + if (item != null && !MaterialUtil.isAir(item.getType()) && item.hasItemMeta() && item.getItemMeta().hasDisplayName()) { return Lists.newArrayList(FormatUtil.unformatString(user, "essentials.itemname", item.getItemMeta().getDisplayName())); } } diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandpowertool.java b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandpowertool.java index d6ba3fa0e..b21d1269b 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandpowertool.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandpowertool.java @@ -2,6 +2,7 @@ package com.earth2me.essentials.commands; import com.earth2me.essentials.CommandSource; import com.earth2me.essentials.User; +import com.earth2me.essentials.craftbukkit.Inventories; import com.earth2me.essentials.utils.StringUtil; import com.google.common.collect.Lists; import org.bukkit.Material; @@ -22,7 +23,7 @@ public class Commandpowertool extends EssentialsCommand { @Override protected void run(final Server server, final User user, final String commandLabel, final String[] args) throws Exception { final String command = getFinalArg(args, 0); - final ItemStack itemStack = user.getBase().getItemInHand(); + final ItemStack itemStack = Inventories.getItemInHand(user.getBase()); powertool(user.getSource(), user, itemStack, command); } @@ -111,7 +112,7 @@ public class Commandpowertool extends EssentialsCommand { } try { - final ItemStack itemStack = user.getBase().getItemInHand(); + final ItemStack itemStack = Inventories.getItemInHand(user.getBase()); final List powertools = user.getPowertool(itemStack); for (final String tool : powertools) { options.add("r:" + tool); diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandrepair.java b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandrepair.java index 834e7a3bc..e466c7e6d 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandrepair.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandrepair.java @@ -3,6 +3,7 @@ package com.earth2me.essentials.commands; import com.earth2me.essentials.ChargeException; import com.earth2me.essentials.Trade; import com.earth2me.essentials.User; +import com.earth2me.essentials.craftbukkit.Inventories; import com.earth2me.essentials.utils.MaterialUtil; import com.earth2me.essentials.utils.StringUtil; import com.earth2me.essentials.utils.VersionUtil; @@ -62,7 +63,7 @@ public class Commandrepair extends EssentialsCommand { public void repairAll(final User user) throws Exception { final List repaired = new ArrayList<>(); - repairItems(user.getBase().getInventory().getContents(), user, repaired); + repairItems(Inventories.getInventory(user.getBase(), false), user, repaired); if (user.isAuthorized("essentials.repair.armor")) { repairItems(user.getBase().getInventory().getArmorContents(), user, repaired); diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandsell.java b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandsell.java index f9cd09bf5..98b19c04a 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandsell.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandsell.java @@ -2,6 +2,7 @@ package com.earth2me.essentials.commands; import com.earth2me.essentials.Trade; import com.earth2me.essentials.User; +import com.earth2me.essentials.craftbukkit.Inventories; import com.earth2me.essentials.utils.NumberUtil; import com.google.common.collect.Lists; import net.ess3.api.events.UserBalanceUpdateEvent; @@ -108,11 +109,11 @@ public class Commandsell extends EssentialsCommand { //TODO: Prices for Enchantments final ItemStack ris = is.clone(); ris.setAmount(amount); - if (!user.getBase().getInventory().containsAtLeast(ris, amount)) { + if (!Inventories.containsAtLeast(user.getBase(), ris, amount)) { // This should never happen. throw new IllegalStateException("Trying to remove more items than are available."); } - user.getBase().getInventory().removeItem(ris); + Inventories.removeItemAmount(user.getBase(), ris, ris.getAmount()); user.getBase().updateInventory(); Trade.log("Command", "Sell", "Item", user.getName(), new Trade(ris, ess), user.getName(), new Trade(result, ess), user.getLocation(), user.getMoney(), ess); user.giveMoney(result, null, UserBalanceUpdateEvent.Cause.COMMAND_SELL); diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandsetworth.java b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandsetworth.java index 19b4fd10e..baa01a4bc 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandsetworth.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandsetworth.java @@ -22,7 +22,7 @@ public class Commandsetworth extends EssentialsCommand { final ItemStack stack; final String price; if (args.length == 1) { - stack = user.getBase().getInventory().getItemInHand(); + stack = user.getItemInHand(); price = args[0]; } else { stack = ess.getItemDb().get(args[0]); diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandskull.java b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandskull.java index af5d3a20a..4e1065548 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandskull.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandskull.java @@ -1,7 +1,7 @@ package com.earth2me.essentials.commands; import com.earth2me.essentials.User; -import com.earth2me.essentials.craftbukkit.InventoryWorkaround; +import com.earth2me.essentials.craftbukkit.Inventories; import com.earth2me.essentials.utils.EnumUtil; import com.earth2me.essentials.utils.MaterialUtil; import com.google.common.collect.Lists; @@ -9,7 +9,6 @@ import org.bukkit.Material; import org.bukkit.Server; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.SkullMeta; -import org.bukkit.scheduler.BukkitRunnable; import java.util.Collections; import java.util.List; @@ -60,26 +59,21 @@ public class Commandskull extends EssentialsCommand { } private void editSkull(final User user, final ItemStack stack, final SkullMeta skullMeta, final String owner, final boolean spawn) { - new BukkitRunnable() { - @Override - public void run() { - //Run this stuff async because SkullMeta#setOwner causes a http request. - skullMeta.setDisplayName("§fSkull of " + owner); - skullMeta.setOwner(owner); - new BukkitRunnable() { - @Override - public void run() { - stack.setItemMeta(skullMeta); - if (spawn) { - InventoryWorkaround.addItems(user.getBase().getInventory(), stack); - user.sendMessage(tl("givenSkull", owner)); - return; - } - user.sendMessage(tl("skullChanged", owner)); - } - }.runTask(ess); - } - }.runTaskAsynchronously(ess); + ess.runTaskAsynchronously(() -> { + //Run this stuff async because SkullMeta#setOwner causes a http request. + skullMeta.setDisplayName("§fSkull of " + owner); + //noinspection deprecation + skullMeta.setOwner(owner); + ess.scheduleSyncDelayedTask(() -> { + stack.setItemMeta(skullMeta); + if (spawn) { + Inventories.addItem(user.getBase(), stack); + user.sendMessage(tl("givenSkull", owner)); + return; + } + user.sendMessage(tl("skullChanged", owner)); + }); + }); } @Override diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandunlimited.java b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandunlimited.java index db740e6ff..8e8134638 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandunlimited.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandunlimited.java @@ -1,6 +1,7 @@ package com.earth2me.essentials.commands; import com.earth2me.essentials.User; +import com.earth2me.essentials.craftbukkit.Inventories; import org.bukkit.Material; import org.bukkit.Server; import org.bukkit.inventory.ItemStack; @@ -75,8 +76,8 @@ public class Commandunlimited extends EssentialsCommand { if (!target.hasUnlimited(stack)) { message = "enableUnlimited"; enableUnlimited = true; - if (!target.getBase().getInventory().containsAtLeast(stack, stack.getAmount())) { - target.getBase().getInventory().addItem(stack); + if (!Inventories.containsAtLeast(target.getBase(), stack, stack.getAmount())) { + Inventories.addItem(target.getBase(), stack); } } diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandwhois.java b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandwhois.java index f2a3acf7f..ae5c7c683 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandwhois.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandwhois.java @@ -40,7 +40,7 @@ public class Commandwhois extends EssentialsCommand { sender.sendMessage(tl("whoisHunger", user.getBase().getFoodLevel(), user.getBase().getSaturation())); sender.sendMessage(tl("whoisExp", SetExpFix.getTotalExperience(user.getBase()), user.getBase().getLevel())); sender.sendMessage(tl("whoisLocation", user.getLocation().getWorld().getName(), user.getLocation().getBlockX(), user.getLocation().getBlockY(), user.getLocation().getBlockZ())); - final long playtimeMs = System.currentTimeMillis() - (user.getBase().getStatistic(PLAY_ONE_TICK) * 50); + final long playtimeMs = System.currentTimeMillis() - (user.getBase().getStatistic(PLAY_ONE_TICK) * 50L); sender.sendMessage(tl("whoisPlaytime", DateUtil.formatDateDiff(playtimeMs))); if (!ess.getSettings().isEcoDisabled()) { sender.sendMessage(tl("whoisMoney", NumberUtil.displayCurrency(user.getMoney(), ess))); diff --git a/Essentials/src/main/java/com/earth2me/essentials/config/EssentialsConfiguration.java b/Essentials/src/main/java/com/earth2me/essentials/config/EssentialsConfiguration.java index e53fa07a8..cf11ee913 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/config/EssentialsConfiguration.java +++ b/Essentials/src/main/java/com/earth2me/essentials/config/EssentialsConfiguration.java @@ -33,7 +33,9 @@ import java.lang.reflect.Type; import java.math.BigDecimal; import java.nio.file.Files; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -303,6 +305,19 @@ public class EssentialsConfiguration { return ConfigurateUtil.getMap(configurationNode); } + public Map getStringMap(String path) { + final CommentedConfigurationNode node = getInternal(path); + if (node == null || !node.isMap()) { + return Collections.emptyMap(); + } + + final Map map = new LinkedHashMap<>(); + for (Map.Entry entry : node.childrenMap().entrySet()) { + map.put(String.valueOf(entry.getKey()), String.valueOf(entry.getValue().rawScalar())); + } + return map; + } + public void removeProperty(String path) { final CommentedConfigurationNode node = getInternal(path); if (node != null) { diff --git a/Essentials/src/main/java/com/earth2me/essentials/craftbukkit/Inventories.java b/Essentials/src/main/java/com/earth2me/essentials/craftbukkit/Inventories.java new file mode 100644 index 000000000..587059056 --- /dev/null +++ b/Essentials/src/main/java/com/earth2me/essentials/craftbukkit/Inventories.java @@ -0,0 +1,404 @@ +package com.earth2me.essentials.craftbukkit; + +import com.earth2me.essentials.utils.MaterialUtil; +import com.earth2me.essentials.utils.VersionUtil; +import org.bukkit.entity.Player; +import org.bukkit.inventory.EntityEquipment; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; + +public final class Inventories { + private static final int HELM_SLOT = 39; + private static final int CHEST_SLOT = 38; + private static final int LEG_SLOT = 37; + private static final int BOOT_SLOT = 36; + private static final boolean HAS_OFFHAND = VersionUtil.getServerBukkitVersion().isHigherThanOrEqualTo(VersionUtil.v1_9_R01); + + private Inventories() { + } + + public static ItemStack getItemInHand(final Player player) { + if (!HAS_OFFHAND) { + //noinspection deprecation + return player.getInventory().getItemInHand(); + } + final PlayerInventory inventory = player.getInventory(); + final ItemStack main = inventory.getItemInMainHand(); + return !isEmpty(main) ? main : inventory.getItemInOffHand(); + } + + public static ItemStack getItemInMainHand(final Player player) { + if (!HAS_OFFHAND) { + //noinspection deprecation + return player.getInventory().getItemInHand(); + } + return player.getInventory().getItemInMainHand(); + } + + public static void setItemInMainHand(final Player player, final ItemStack stack) { + if (HAS_OFFHAND) { + player.getInventory().setItemInMainHand(stack); + } else { + //noinspection deprecation + player.setItemInHand(stack); + } + } + + public static void setItemInMainHand(final EntityEquipment entityEquipment, final ItemStack stack) { + if (HAS_OFFHAND) { + entityEquipment.setItemInMainHand(stack); + } else { + //noinspection deprecation + entityEquipment.setItemInHand(stack); + } + } + + public static void setItemInMainHandDropChance(final EntityEquipment entityEquipment, final float chance) { + if (HAS_OFFHAND) { + entityEquipment.setItemInMainHandDropChance(chance); + } else { + //noinspection deprecation + entityEquipment.setItemInHandDropChance(chance); + } + } + + public static boolean containsAtLeast(final Player player, final ItemStack item, int amount) { + for (final ItemStack invItem : player.getInventory().getContents()) { + if (isEmpty(invItem)) { + continue; + } + if (invItem.isSimilar(item)) { + amount -= invItem.getAmount(); + if (amount <= 0) { + return true; + } + } + } + return false; + } + + public static boolean hasSpace(final Player player, final int maxStack, final boolean includeArmor, ItemStack... items) { + items = normalizeItems(cloneItems(items)); + final InventoryData inventoryData = parseInventoryData(player.getInventory(), items, maxStack, includeArmor); + + final List emptySlots = inventoryData.getEmptySlots(); + for (final ItemStack item : items) { + if (isEmpty(item)) { + continue; + } + + final int itemMax = Math.max(maxStack, item.getMaxStackSize()); + final List partialSlots = inventoryData.getPartialSlots().get(item); + while (true) { + if (partialSlots == null || partialSlots.isEmpty()) { + if (emptySlots.isEmpty()) { + return false; + } + + emptySlots.remove(0); + if (item.getAmount() > itemMax) { + item.setAmount(item.getAmount() - itemMax); + } else { + break; + } + } else { + final int slot = partialSlots.remove(0); + ItemStack existing = player.getInventory().getItem(slot); + if (isEmpty(existing)) { + existing = item.clone(); + existing.setAmount(0); + } + + final int amount = item.getAmount(); + final int existingAmount = existing.getAmount(); + + if (amount + existingAmount <= itemMax) { + break; + } else { + item.setAmount(amount + existingAmount - itemMax); + } + } + } + } + + return true; + } + + public static Map addItem(final Player player, final ItemStack... items) { + return addItem(player, 0, false, items); + } + + public static Map addItem(final Player player, final int maxStack, final ItemStack... items) { + return addItem(player, maxStack, false, items); + } + + public static Map addItem(final Player player, final int maxStack, final boolean allowArmor, ItemStack... items) { + items = normalizeItems(cloneItems(items)); + final Map leftover = new HashMap<>(); + final InventoryData inventoryData = parseInventoryData(player.getInventory(), items, maxStack, allowArmor); + + final List emptySlots = inventoryData.getEmptySlots(); + for (int i = 0; i < items.length; i++) { + final ItemStack item = items[i]; + if (isEmpty(item)) { + continue; + } + + final int itemMax = Math.max(maxStack, item.getMaxStackSize()); + final List partialSlots = inventoryData.getPartialSlots().get(item); + while (true) { + if (partialSlots == null || partialSlots.isEmpty()) { + if (emptySlots.isEmpty()) { + leftover.put(i, item); + break; + } + + final int slot = emptySlots.remove(0); + if (item.getAmount() > itemMax) { + final ItemStack split = item.clone(); + split.setAmount(itemMax); + player.getInventory().setItem(slot, split); + item.setAmount(item.getAmount() - itemMax); + } else { + player.getInventory().setItem(slot, item); + break; + } + } else { + final int slot = partialSlots.remove(0); + ItemStack existing = player.getInventory().getItem(slot); + if (isEmpty(existing)) { + existing = item.clone(); + existing.setAmount(0); + } + + final int amount = item.getAmount(); + final int existingAmount = existing.getAmount(); + + if (amount + existingAmount <= itemMax) { + existing.setAmount(amount + existingAmount); + player.getInventory().setItem(slot, existing); + break; + } else { + existing.setAmount(itemMax); + player.getInventory().setItem(slot, existing); + item.setAmount(amount + existingAmount - itemMax); + } + } + } + } + + return leftover; + } + + public static ItemStack[] getInventory(final Player player, final boolean includeArmor) { + final ItemStack[] items = new ItemStack[41]; + for (int i = 0; i < items.length; i++) { + if (!includeArmor && isArmorSlot(i)) { + items[i] = null; + continue; + } + + items[i] = player.getInventory().getItem(i); + } + + return items; + } + + public static void removeItemExact(final Player player, final ItemStack toRemove, final boolean includeArmor) { + removeItems(player, itemStack -> itemStack.equals(toRemove), includeArmor); + } + + public static int removeItemSimilar(final Player player, final ItemStack toRemove, final boolean includeArmor) { + return removeItems(player, itemStack -> itemStack.isSimilar(toRemove), includeArmor); + } + + public static int removeItems(final Player player, final Predicate removePredicate, final boolean includeArmor) { + int removedAmount = 0; + final ItemStack[] items = player.getInventory().getContents(); + for (int i = 0; i < items.length; i++) { + if (!includeArmor && isArmorSlot(i)) { + continue; + } + + final ItemStack item = items[i]; + if (isEmpty(item)) { + continue; + } + + if (removePredicate.test(item)) { + removedAmount += item.getAmount(); + item.setAmount(0); + player.getInventory().setItem(i, item); + } + } + return removedAmount; + } + + public static boolean removeItemAmount(final Player player, final ItemStack toRemove, int amount) { + final List clearSlots = new ArrayList<>(); + final ItemStack[] items = player.getInventory().getContents(); + + for (int i = 0; i < items.length; i++) { + final ItemStack item = items[i]; + if (isEmpty(item)) { + continue; + } + + if (item.isSimilar(toRemove)) { + if (item.getAmount() >= amount) { + item.setAmount(item.getAmount() - amount); + player.getInventory().setItem(i, item); + for (final int slot : clearSlots) { + clearSlot(player, slot); + } + return true; + } else { + amount -= item.getAmount(); + clearSlots.add(i); + } + + if (amount == 0) { + for (final int slot : clearSlots) { + clearSlot(player, slot); + } + return true; + } + } + } + return false; + } + + public static void clearSlot(final Player player, final int slot) { + final ItemStack item = player.getInventory().getItem(slot); + if (!isEmpty(item)) { + item.setAmount(0); + player.getInventory().setItem(slot, item); + } + } + + public static void setSlot(final Player inventory, final int slot, final ItemStack item) { + inventory.getInventory().setItem(slot, item); + } + + private static ItemStack[] normalizeItems(final ItemStack[] items) { + if (items.length <= 1) { + return items; + } + + final ItemStack[] normalizedItems = new ItemStack[items.length]; + int nextNormalizedIndex = 0; + inputLoop: + for (final ItemStack item : items) { + if (isEmpty(item)) { + continue; + } + + for (int j = 0; j < nextNormalizedIndex; j++) { + final ItemStack normalizedItem = normalizedItems[j]; + if (isEmpty(normalizedItem)) { + continue; + } + + if (item.isSimilar(normalizedItem)) { + normalizedItem.setAmount(normalizedItem.getAmount() + item.getAmount()); + continue inputLoop; + } + } + normalizedItems[nextNormalizedIndex++] = item; + } + + return normalizedItems; + } + + private static ItemStack[] cloneItems(final ItemStack[] items) { + final ItemStack[] clonedItems = new ItemStack[items.length]; + for (int i = 0; i < items.length; i++) { + final ItemStack item = items[i]; + if (isEmpty(item)) { + continue; + } + + clonedItems[i] = item.clone(); + } + + return clonedItems; + } + + private static InventoryData parseInventoryData(final Inventory inventory, final ItemStack[] items, final int maxStack, final boolean includeArmor) { + final ItemStack[] inventoryContents = inventory.getContents(); + final List emptySlots = new ArrayList<>(); + final HashMap> partialSlots = new HashMap<>(); + + for (int i = 0; i < inventoryContents.length; i++) { + if (!includeArmor && isArmorSlot(i)) { + continue; + } + + final ItemStack invItem = inventoryContents[i]; + if (isEmpty(invItem)) { + emptySlots.add(i); + } else { + for (final ItemStack newItem : items) { + if (invItem.getAmount() < Math.max(maxStack, invItem.getMaxStackSize()) && invItem.isSimilar(newItem)) { + partialSlots.computeIfAbsent(newItem, k -> new ArrayList<>()).add(i); + } + } + } + } + + // Convert empty armor slots to partial slots if we have armor items in the inventory, otherwise remove them from the empty slots. + if (includeArmor) { + ItemStack helm = null; + ItemStack chest = null; + ItemStack legs = null; + ItemStack boots = null; + for (final ItemStack item : items) { + if (isEmpty(item)) { + continue; + } + if (helm == null && MaterialUtil.isHelmet(item.getType())) { + helm = item; + if (emptySlots.contains(HELM_SLOT)) { + partialSlots.computeIfAbsent(helm, k -> new ArrayList<>()).add(HELM_SLOT); + } + } else if (chest == null && MaterialUtil.isChestplate(item.getType())) { + chest = item; + if (emptySlots.contains(CHEST_SLOT)) { + partialSlots.computeIfAbsent(chest, k -> new ArrayList<>()).add(CHEST_SLOT); + } + } else if (legs == null && MaterialUtil.isLeggings(item.getType())) { + legs = item; + if (emptySlots.contains(LEG_SLOT)) { + partialSlots.computeIfAbsent(legs, k -> new ArrayList<>()).add(LEG_SLOT); + } + } else if (boots == null && MaterialUtil.isBoots(item.getType())) { + boots = item; + if (emptySlots.contains(BOOT_SLOT)) { + partialSlots.computeIfAbsent(boots, k -> new ArrayList<>()).add(BOOT_SLOT); + } + } + } + emptySlots.remove((Object) HELM_SLOT); + emptySlots.remove((Object) CHEST_SLOT); + emptySlots.remove((Object) LEG_SLOT); + emptySlots.remove((Object) BOOT_SLOT); + } + + return new InventoryData(emptySlots, partialSlots); + } + + private static boolean isEmpty(final ItemStack stack) { + return stack == null || MaterialUtil.isAir(stack.getType()); + } + + private static boolean isArmorSlot(final int slot) { + return slot == HELM_SLOT || slot == CHEST_SLOT || slot == LEG_SLOT || slot == BOOT_SLOT; + } +} diff --git a/Essentials/src/main/java/com/earth2me/essentials/craftbukkit/InventoryData.java b/Essentials/src/main/java/com/earth2me/essentials/craftbukkit/InventoryData.java new file mode 100644 index 000000000..a26e9cc7e --- /dev/null +++ b/Essentials/src/main/java/com/earth2me/essentials/craftbukkit/InventoryData.java @@ -0,0 +1,24 @@ +package com.earth2me.essentials.craftbukkit; + +import org.bukkit.inventory.ItemStack; + +import java.util.HashMap; +import java.util.List; + +public class InventoryData { + private final List emptySlots; + private final HashMap> partialSlots; + + public InventoryData(List emptySlots, HashMap> partialSlots) { + this.emptySlots = emptySlots; + this.partialSlots = partialSlots; + } + + public List getEmptySlots() { + return emptySlots; + } + + public HashMap> getPartialSlots() { + return partialSlots; + } +} diff --git a/Essentials/src/main/java/com/earth2me/essentials/craftbukkit/InventoryWorkaround.java b/Essentials/src/main/java/com/earth2me/essentials/craftbukkit/InventoryWorkaround.java deleted file mode 100644 index 1e4062cb2..000000000 --- a/Essentials/src/main/java/com/earth2me/essentials/craftbukkit/InventoryWorkaround.java +++ /dev/null @@ -1,245 +0,0 @@ -package com.earth2me.essentials.craftbukkit; - -import com.earth2me.essentials.utils.VersionUtil; -import org.bukkit.Bukkit; -import org.bukkit.Material; -import org.bukkit.entity.Player; -import org.bukkit.inventory.EntityEquipment; -import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.PlayerInventory; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; - -/* - * This class can be removed when https://github.com/Bukkit/CraftBukkit/pull/193 is accepted to CraftBukkit - */ -public final class InventoryWorkaround { - /* - Spigot 1.9, for whatever reason, decided to merge the armor and main player inventories without providing a way - to access the main inventory. There's lots of ugly code in here to work around that. - */ - private static final int USABLE_PLAYER_INV_SIZE = 36; - private static final boolean IS_OFFHAND = VersionUtil.getServerBukkitVersion().isHigherThanOrEqualTo(VersionUtil.v1_9_R01); - - private InventoryWorkaround() { - } - - private static int firstPartial(final Inventory inventory, final ItemStack item, final int maxAmount) { - if (item == null) { - return -1; - } - final ItemStack[] stacks = inventory.getContents(); - for (int i = 0; i < stacks.length; i++) { - final ItemStack cItem = stacks[i]; - if (cItem != null && cItem.getAmount() < maxAmount && cItem.isSimilar(item)) { - return i; - } - } - return -1; - } - - private static boolean isCombinedInventory(final Inventory inventory) { - return inventory instanceof PlayerInventory && inventory.getContents().length > USABLE_PLAYER_INV_SIZE; - } - - // Clears inventory without clearing armor - public static void clearInventoryNoArmor(final PlayerInventory inventory) { - if (isCombinedInventory(inventory)) { - for (int i = 0; i < USABLE_PLAYER_INV_SIZE; i++) { - inventory.setItem(i, null); - } - } else { - inventory.clear(); - } - } - - private static Inventory makeTruncatedPlayerInventory(final PlayerInventory playerInventory) { - final Inventory fakeInventory = Bukkit.getServer().createInventory(null, USABLE_PLAYER_INV_SIZE); - fakeInventory.setContents(Arrays.copyOf(playerInventory.getContents(), fakeInventory.getSize())); - return fakeInventory; - } - - // Returns what it couldn't store - // This will will abort if it couldn't store all items - public static Map addAllItems(final Inventory inventory, final ItemStack... items) { - final ItemStack[] contents = inventory.getContents(); - - final Inventory fakeInventory; - if (isCombinedInventory(inventory)) { - fakeInventory = makeTruncatedPlayerInventory((PlayerInventory) inventory); - } else { - fakeInventory = Bukkit.getServer().createInventory(null, inventory.getType()); - fakeInventory.setContents(contents); - } - final Map overflow = addItems(fakeInventory, items); - if (overflow.isEmpty()) { - addItems(inventory, items); - return null; - } - return addItems(fakeInventory, items); - } - - public static Map addAllOversizedItems(final Inventory inventory, final int oversizedStacks, final ItemStack... items) { - final ItemStack[] contents = inventory.getContents(); - - final Inventory fakeInventory; - if (isCombinedInventory(inventory)) { - fakeInventory = makeTruncatedPlayerInventory((PlayerInventory) inventory); - } else { - fakeInventory = Bukkit.getServer().createInventory(null, inventory.getType()); - fakeInventory.setContents(contents); - } - final Map overflow = addOversizedItems(fakeInventory, oversizedStacks, items); - if (overflow.isEmpty()) { - addOversizedItems(inventory, oversizedStacks, items); - return null; - } - return overflow; - } - - // Returns what it couldn't store - public static Map addItems(final Inventory inventory, final ItemStack... items) { - return addOversizedItems(inventory, 0, items); - } - - // Returns what it couldn't store - // Set oversizedStack to below normal stack size to disable oversized stacks - public static Map addOversizedItems(final Inventory inventory, final int oversizedStacks, final ItemStack... items) { - if (isCombinedInventory(inventory)) { - final Inventory fakeInventory = makeTruncatedPlayerInventory((PlayerInventory) inventory); - final Map overflow = addOversizedItems(fakeInventory, oversizedStacks, items); - for (int i = 0; i < fakeInventory.getContents().length; i++) { - inventory.setItem(i, fakeInventory.getContents()[i]); - } - return overflow; - } - - final Map leftover = new HashMap<>(); - - /* - * TODO: some optimization - Create a 'firstPartial' with a 'fromIndex' - Record the lastPartial per Material - - * Cache firstEmpty result - */ - - // combine items - - final ItemStack[] combined = new ItemStack[items.length]; - for (final ItemStack item : items) { - if (item == null || item.getAmount() < 1) { - continue; - } - for (int j = 0; j < combined.length; j++) { - if (combined[j] == null) { - combined[j] = item.clone(); - break; - } - if (combined[j].isSimilar(item)) { - combined[j].setAmount(combined[j].getAmount() + item.getAmount()); - break; - } - } - } - - for (int i = 0; i < combined.length; i++) { - final ItemStack item = combined[i]; - if (item == null || item.getType() == Material.AIR) { - continue; - } - - while (true) { - // Do we already have a stack of it? - final int maxAmount = Math.max(oversizedStacks, item.getType().getMaxStackSize()); - final int firstPartial = firstPartial(inventory, item, maxAmount); - - // Drat! no partial stack - if (firstPartial == -1) { - // Find a free spot! - final int firstFree = inventory.firstEmpty(); - - if (firstFree == -1) { - // No space at all! - leftover.put(i, item); - break; - } else { - // More than a single stack! - if (item.getAmount() > maxAmount) { - final ItemStack stack = item.clone(); - stack.setAmount(maxAmount); - inventory.setItem(firstFree, stack); - item.setAmount(item.getAmount() - maxAmount); - } else { - // Just store it - inventory.setItem(firstFree, item); - break; - } - } - } else { - // So, apparently it might only partially fit, well lets do just that - final ItemStack partialItem = inventory.getItem(firstPartial); - - final int amount = item.getAmount(); - final int partialAmount = partialItem.getAmount(); - - // Check if it fully fits - if (amount + partialAmount <= maxAmount) { - partialItem.setAmount(amount + partialAmount); - break; - } - - // It fits partially - partialItem.setAmount(maxAmount); - item.setAmount(amount + partialAmount - maxAmount); - } - } - } - return leftover; - } - - @SuppressWarnings("deprecation") - public static void setItemInMainHand(final Player p, final ItemStack item) { - if (IS_OFFHAND) { - p.getInventory().setItemInMainHand(item); - } else { - p.setItemInHand(item); - } - } - - @SuppressWarnings("deprecation") - public static void setItemInMainHand(final EntityEquipment invent, final ItemStack item) { - if (IS_OFFHAND) { - invent.setItemInMainHand(item); - } else { - invent.setItemInHand(item); - } - } - - @SuppressWarnings("deprecation") - public static void setItemInMainHandDropChance(final EntityEquipment invent, final float chance) { - if (IS_OFFHAND) { - invent.setItemInMainHandDropChance(chance); - } else { - invent.setItemInHandDropChance(chance); - } - } - - public static void setItemInOffHand(final Player p, final ItemStack item) { - if (IS_OFFHAND) { - p.getInventory().setItemInOffHand(item); - } - } - - public static int clearItemInOffHand(final Player p, final ItemStack item) { - if (IS_OFFHAND) { - int removedAmount = 0; - if (p.getInventory().getItemInOffHand().getType().equals(item.getType())) { - removedAmount = p.getInventory().getItemInOffHand().getAmount(); - p.getInventory().setItemInOffHand(null); - } - return removedAmount; - } - return 0; - } -} diff --git a/Essentials/src/main/java/com/earth2me/essentials/items/AbstractItemDb.java b/Essentials/src/main/java/com/earth2me/essentials/items/AbstractItemDb.java index 4feb311c1..c650c8990 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/items/AbstractItemDb.java +++ b/Essentials/src/main/java/com/earth2me/essentials/items/AbstractItemDb.java @@ -2,6 +2,7 @@ package com.earth2me.essentials.items; import com.earth2me.essentials.IConf; import com.earth2me.essentials.User; +import com.earth2me.essentials.craftbukkit.Inventories; import com.earth2me.essentials.utils.FormatUtil; import com.earth2me.essentials.utils.MaterialUtil; import com.earth2me.essentials.utils.VersionUtil; @@ -156,14 +157,14 @@ public abstract class AbstractItemDb implements IConf, net.ess3.api.IItemDb { } else if (args[0].equalsIgnoreCase("hand")) { is.add(user.getItemInHand().clone()); } else if (args[0].equalsIgnoreCase("inventory") || args[0].equalsIgnoreCase("invent") || args[0].equalsIgnoreCase("all")) { - for (final ItemStack stack : user.getBase().getInventory().getContents()) { + for (final ItemStack stack : Inventories.getInventory(user.getBase(), true)) { if (stack == null || stack.getType() == Material.AIR) { continue; } is.add(stack.clone()); } } else if (args[0].equalsIgnoreCase("blocks")) { - for (final ItemStack stack : user.getBase().getInventory().getContents()) { + for (final ItemStack stack : Inventories.getInventory(user.getBase(), true)) { if (stack == null || stack.getType() == Material.AIR || !stack.getType().isBlock()) { continue; } diff --git a/Essentials/src/main/java/com/earth2me/essentials/perm/IPermissionsHandler.java b/Essentials/src/main/java/com/earth2me/essentials/perm/IPermissionsHandler.java index 707be09c8..9897b41ec 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/perm/IPermissionsHandler.java +++ b/Essentials/src/main/java/com/earth2me/essentials/perm/IPermissionsHandler.java @@ -3,6 +3,7 @@ package com.earth2me.essentials.perm; import com.earth2me.essentials.Essentials; import com.earth2me.essentials.User; import com.earth2me.essentials.utils.TriState; +import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; import java.util.List; @@ -10,9 +11,15 @@ import java.util.function.Function; import java.util.function.Supplier; public interface IPermissionsHandler { - String getGroup(Player base); + boolean addToGroup(OfflinePlayer base, String group); - List getGroups(Player base); + boolean removeFromGroup(OfflinePlayer base, String group); + + String getGroup(OfflinePlayer base); + + List getGroups(OfflinePlayer base); + + List getGroups(); boolean canBuild(Player base, String group); diff --git a/Essentials/src/main/java/com/earth2me/essentials/perm/PermissionsHandler.java b/Essentials/src/main/java/com/earth2me/essentials/perm/PermissionsHandler.java index 722c55a9a..70e4a9fb3 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/perm/PermissionsHandler.java +++ b/Essentials/src/main/java/com/earth2me/essentials/perm/PermissionsHandler.java @@ -10,8 +10,10 @@ import com.earth2me.essentials.perm.impl.ModernVaultHandler; import com.earth2me.essentials.perm.impl.SuperpermsHandler; import com.earth2me.essentials.utils.TriState; import com.google.common.collect.ImmutableSet; +import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -34,7 +36,7 @@ public class PermissionsHandler implements IPermissionsHandler { } @Override - public String getGroup(final Player base) { + public String getGroup(final OfflinePlayer base) { final long start = System.nanoTime(); String group = handler.getGroup(base); if (group == null) { @@ -45,16 +47,42 @@ public class PermissionsHandler implements IPermissionsHandler { } @Override - public List getGroups(final Player base) { + public List getGroups(final OfflinePlayer base) { final long start = System.nanoTime(); - List groups = handler.getGroups(base); + final List groups = new ArrayList<>(); + groups.add(defaultGroup); + groups.addAll(handler.getGroups(base)); + checkPermLag(start, String.format("Getting groups for %s", base.getName())); + return Collections.unmodifiableList(groups); + } + + @Override + public List getGroups() { + final long start = System.nanoTime(); + List groups = handler.getGroups(); if (groups == null || groups.isEmpty()) { groups = Collections.singletonList(defaultGroup); } - checkPermLag(start, String.format("Getting groups for %s", base.getName())); + checkPermLag(start, "Getting all groups"); return Collections.unmodifiableList(groups); } + @Override + public boolean addToGroup(OfflinePlayer base, String group) { + final long start = System.nanoTime(); + final boolean result = handler.addToGroup(base, group); + checkPermLag(start, String.format("Adding group to %s", base.getName())); + return result; + } + + @Override + public boolean removeFromGroup(OfflinePlayer base, String group) { + final long start = System.nanoTime(); + final boolean result = handler.removeFromGroup(base, group); + checkPermLag(start, String.format("Removing group from %s", base.getName())); + return result; + } + @Override public boolean canBuild(final Player base, final String group) { return handler.canBuild(base, group); diff --git a/Essentials/src/main/java/com/earth2me/essentials/perm/impl/AbstractVaultHandler.java b/Essentials/src/main/java/com/earth2me/essentials/perm/impl/AbstractVaultHandler.java index 9f2777f7d..3e7f745c6 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/perm/impl/AbstractVaultHandler.java +++ b/Essentials/src/main/java/com/earth2me/essentials/perm/impl/AbstractVaultHandler.java @@ -3,6 +3,7 @@ package com.earth2me.essentials.perm.impl; import net.milkbowl.vault.chat.Chat; import net.milkbowl.vault.permission.Permission; import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; import org.bukkit.plugin.RegisteredServiceProvider; import org.bukkit.plugin.java.JavaPlugin; @@ -32,13 +33,34 @@ public abstract class AbstractVaultHandler extends SuperpermsHandler { } @Override - public String getGroup(final Player base) { - return perms.getPrimaryGroup(base); + public String getGroup(final OfflinePlayer base) { + if (base.isOnline()) { + return perms.getPrimaryGroup(base.getPlayer()); + } + return perms.getPrimaryGroup(null, base); } @Override - public List getGroups(final Player base) { - return Arrays.asList(perms.getPlayerGroups(base)); + public List getGroups(final OfflinePlayer base) { + if (base.isOnline()) { + return Arrays.asList(perms.getPlayerGroups(base.getPlayer())); + } + return Arrays.asList(perms.getPlayerGroups(null, base)); + } + + @Override + public List getGroups() { + return Arrays.asList(perms.getGroups()); + } + + @Override + public boolean addToGroup(OfflinePlayer base, String group) { + return perms.playerAddGroup(null, base, group); + } + + @Override + public boolean removeFromGroup(OfflinePlayer base, String group) { + return perms.playerRemoveGroup(null, base, group); } @Override diff --git a/Essentials/src/main/java/com/earth2me/essentials/perm/impl/SuperpermsHandler.java b/Essentials/src/main/java/com/earth2me/essentials/perm/impl/SuperpermsHandler.java index 1e285b8bb..a0e355e26 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/perm/impl/SuperpermsHandler.java +++ b/Essentials/src/main/java/com/earth2me/essentials/perm/impl/SuperpermsHandler.java @@ -5,6 +5,7 @@ import com.earth2me.essentials.User; import com.earth2me.essentials.perm.IPermissionsHandler; import com.earth2me.essentials.utils.TriState; import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; import org.bukkit.permissions.Permission; import org.bukkit.permissions.PermissionAttachmentInfo; @@ -21,12 +22,27 @@ public class SuperpermsHandler implements IPermissionsHandler { } @Override - public String getGroup(final Player base) { + public boolean addToGroup(OfflinePlayer base, String group) { + return false; + } + + @Override + public boolean removeFromGroup(OfflinePlayer base, String group) { + return false; + } + + @Override + public String getGroup(final OfflinePlayer base) { return null; } @Override - public List getGroups(final Player base) { + public List getGroups(final OfflinePlayer base) { + return null; + } + + @Override + public List getGroups() { return null; } diff --git a/Essentials/src/main/java/com/earth2me/essentials/signs/SignEnchant.java b/Essentials/src/main/java/com/earth2me/essentials/signs/SignEnchant.java index ec86e9df0..51e839802 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/signs/SignEnchant.java +++ b/Essentials/src/main/java/com/earth2me/essentials/signs/SignEnchant.java @@ -4,6 +4,7 @@ import com.earth2me.essentials.ChargeException; import com.earth2me.essentials.Enchantments; import com.earth2me.essentials.Trade; import com.earth2me.essentials.User; +import com.earth2me.essentials.craftbukkit.Inventories; import net.ess3.api.IEssentials; import net.ess3.provider.MaterialTagProvider; import org.bukkit.enchantments.Enchantment; @@ -66,7 +67,7 @@ public class SignEnchant extends EssentialsSign { @Override protected boolean onSignInteract(final ISign sign, final User player, final String username, final IEssentials ess) throws SignException, ChargeException { - final ItemStack playerHand = player.getBase().getItemInHand(); + final ItemStack playerHand = Inventories.getItemInHand(player.getBase()); final MaterialTagProvider tagProvider = ess.getProviders().get(MaterialTagProvider.class); final String itemName = sign.getLine(1); final ItemStack search = itemName.equals("*") || itemName.equalsIgnoreCase("any") || (tagProvider != null && tagProvider.tagExists(itemName) && tagProvider.isTagged(itemName, playerHand.getType())) ? null : getItemStack(itemName, 1, ess); diff --git a/Essentials/src/main/java/com/earth2me/essentials/signs/SignTrade.java b/Essentials/src/main/java/com/earth2me/essentials/signs/SignTrade.java index 33989fcff..96aa6f08d 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/signs/SignTrade.java +++ b/Essentials/src/main/java/com/earth2me/essentials/signs/SignTrade.java @@ -5,6 +5,7 @@ import com.earth2me.essentials.Trade; import com.earth2me.essentials.Trade.OverflowType; import com.earth2me.essentials.Trade.TradeType; import com.earth2me.essentials.User; +import com.earth2me.essentials.craftbukkit.Inventories; import com.earth2me.essentials.utils.MaterialUtil; import com.earth2me.essentials.utils.NumberUtil; import net.ess3.api.IEssentials; @@ -84,10 +85,11 @@ public class SignTrade extends EssentialsSign { private Trade rechargeSign(final ISign sign, final IEssentials ess, final User player) throws SignException, ChargeException { final Trade trade = getTrade(sign, 2, AmountType.COST, false, true, ess); - if (trade.getItemStack() != null && player.getBase().getItemInHand() != null && trade.getItemStack().getType() == player.getBase().getItemInHand().getType() && MaterialUtil.getDamage(trade.getItemStack()) == MaterialUtil.getDamage(player.getBase().getItemInHand()) && trade.getItemStack().getEnchantments().equals(player.getBase().getItemInHand().getEnchantments())) { + ItemStack stack = Inventories.getItemInHand(player.getBase()); + if (trade.getItemStack() != null && stack != null && !MaterialUtil.isAir(stack.getType()) && trade.getItemStack().getType() == stack.getType() && MaterialUtil.getDamage(trade.getItemStack()) == MaterialUtil.getDamage(stack) && trade.getItemStack().getEnchantments().equals(stack.getEnchantments())) { final int amount = trade.getItemStack().getAmount(); - if (player.getBase().getInventory().containsAtLeast(trade.getItemStack(), amount)) { - final ItemStack stack = player.getBase().getItemInHand().clone(); + if (Inventories.containsAtLeast(player.getBase(), trade.getItemStack(), amount)) { + stack = stack.clone(); stack.setAmount(amount); final Trade store = new Trade(stack, ess); addAmount(sign, 2, store, ess); diff --git a/Essentials/src/main/java/com/earth2me/essentials/utils/VersionUtil.java b/Essentials/src/main/java/com/earth2me/essentials/utils/VersionUtil.java index a7b66c2d1..2ddb520b5 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/utils/VersionUtil.java +++ b/Essentials/src/main/java/com/earth2me/essentials/utils/VersionUtil.java @@ -37,8 +37,9 @@ public final class VersionUtil { public static final BukkitVersion v1_18_2_R01 = BukkitVersion.fromString("1.18.2-R0.1-SNAPSHOT"); public static final BukkitVersion v1_19_R01 = BukkitVersion.fromString("1.19-R0.1-SNAPSHOT"); public static final BukkitVersion v1_19_2_R01 = BukkitVersion.fromString("1.19.2-R0.1-SNAPSHOT"); + public static final BukkitVersion v1_19_3_R01 = BukkitVersion.fromString("1.19.3-R0.1-SNAPSHOT"); - private static final Set supportedVersions = ImmutableSet.of(v1_8_8_R01, v1_9_4_R01, v1_10_2_R01, v1_11_2_R01, v1_12_2_R01, v1_13_2_R01, v1_14_4_R01, v1_15_2_R01, v1_16_5_R01, v1_17_1_R01, v1_18_2_R01, v1_19_2_R01); + private static final Set supportedVersions = ImmutableSet.of(v1_8_8_R01, v1_9_4_R01, v1_10_2_R01, v1_11_2_R01, v1_12_2_R01, v1_13_2_R01, v1_14_4_R01, v1_15_2_R01, v1_16_5_R01, v1_17_1_R01, v1_18_2_R01, v1_19_3_R01); public static final boolean PRE_FLATTENING = VersionUtil.getServerBukkitVersion().isLowerThan(VersionUtil.v1_13_0_R01); diff --git a/Essentials/src/main/java/net/essentialsx/api/v2/events/UserMailEvent.java b/Essentials/src/main/java/net/essentialsx/api/v2/events/UserMailEvent.java new file mode 100644 index 000000000..0d9ca4dc7 --- /dev/null +++ b/Essentials/src/main/java/net/essentialsx/api/v2/events/UserMailEvent.java @@ -0,0 +1,58 @@ +package net.essentialsx.api.v2.events; + +import net.ess3.api.IUser; +import net.essentialsx.api.v2.services.mail.MailMessage; +import org.bukkit.event.Cancellable; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; + +/** + * Called when mail is sent to a {@link net.ess3.api.IUser IUser} by another player or the console. + */ +public class UserMailEvent extends Event implements Cancellable { + private static final HandlerList handlers = new HandlerList(); + + private final IUser recipient; + private final MailMessage message; + private boolean canceled; + + public UserMailEvent(IUser recipient, MailMessage message) { + this.recipient = recipient; + this.message = message; + } + + /** + * Gets the recipient of this mail. + * @return the recipient. + */ + public IUser getRecipient() { + return recipient; + } + + /** + * Gets the underlying {@link MailMessage} for this mail. + * @return the message. + */ + public MailMessage getMessage() { + return message; + } + + @Override + public void setCancelled(boolean cancel) { + this.canceled = cancel; + } + + @Override + public boolean isCancelled() { + return canceled; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } +} diff --git a/Essentials/src/main/resources/messages.properties b/Essentials/src/main/resources/messages.properties index 3a2637269..eab2065c5 100644 --- a/Essentials/src/main/resources/messages.properties +++ b/Essentials/src/main/resources/messages.properties @@ -240,6 +240,12 @@ discordbroadcastCommandUsage1Description=Sends the given message to the specifie discordbroadcastInvalidChannel=\u00a74Discord channel \u00a7c{0}\u00a74 does not exist. discordbroadcastPermission=\u00a74You do not have permission to send messages to the \u00a7c{0}\u00a74 channel. discordbroadcastSent=\u00a76Message sent to \u00a7c{0}\u00a76! +discordCommandAccountArgumentUser=The Discord account to look up +discordCommandAccountDescription=Looks up the linked Minecraft account for either yourself or another Discord user +discordCommandAccountResponseLinked=Your account is linked to the Minecraft account: **{0}** +discordCommandAccountResponseLinkedOther={0}'s account is linked to the Minecraft account: **{1}** +discordCommandAccountResponseNotLinked=You do not have a linked Minecraft account. +discordCommandAccountResponseNotLinkedOther={0} does not have a linked Minecraft account. discordCommandDescription=Sends the discord invite link to the player. discordCommandLink=\u00a76Join our Discord server at \u00a7c{0}\u00a76! discordCommandUsage=/ @@ -248,6 +254,14 @@ discordCommandUsage1Description=Sends the discord invite link to the player discordCommandExecuteDescription=Executes a console command on the Minecraft server. discordCommandExecuteArgumentCommand=The command to be executed discordCommandExecuteReply=Executing command: "/{0}" +discordCommandUnlinkDescription=Unlinks the Minecraft account currently linked to your Discord account +discordCommandUnlinkInvalidCode=You do not currently have a Minecraft account linked to Discord! +discordCommandUnlinkUnlinked=Your Discord account has been unlinked from all associated Minecraft accounts. +discordCommandLinkArgumentCode=The code provided in-game to link your Minecraft account +discordCommandLinkDescription=Links your Discord account with your Minecraft account using a code from the in-game /link command +discordCommandLinkHasAccount=You already have an account linked! To unlink your current account, type /unlink. +discordCommandLinkInvalidCode=Invalid linking code! Make sure you've run /link in-game and copied the code correctly. +discordCommandLinkLinked=Successfully linked your account! discordCommandListDescription=Gets a list of online players. discordCommandListArgumentGroup=A specific group to limit your search by discordCommandMessageDescription=Messages a player on the Minecraft server. @@ -265,8 +279,20 @@ discordErrorNoPrimary=You did not define a primary channel or your defined prima discordErrorNoPrimaryPerms=Your bot cannot speak in your primary channel, #{0}. Please make sure your bot has read and write permissions in all channels you wish to use. discordErrorNoToken=No token provided! Please follow the tutorial in the config in order to setup the plugin. discordErrorWebhook=An error occurred while sending messages to your console channel\! This was likely caused by accidentally deleting your console webhook. This can usually by fixed by ensuring your bot has the "Manage Webhooks" permission and running "/ess reload". +discordLinkInvalidGroup=Invalid group {0} was provided for role {1}. The following groups are available: {2} +discordLinkInvalidRole=An invalid role ID, {0}, was provided for group: {1}. You can see the ID of roles with the /roleinfo command in Discord. +discordLinkInvalidRoleInteract=The role, {0} ({1}), cannot be used for group->role synchronization because it above your bot''s upper most role. Either move your bot''s role above "{0}" or move "{0}" below your bot''s role. +discordLinkInvalidRoleManaged=The role, {0} ({1}), cannot be used for group->role synchronization because it is managed by another bot or integration. +discordLinkLinked=\u00a76To link your Minecraft account to Discord, type \u00a7c{0} \u00a76in the Discord server. +discordLinkLinkedAlready=\u00a76You have already linked your Discord account! If you wish to unlink your discord account use \u00a7c/unlink\u00a76. +discordLinkLoginKick=\u00a76You must link your Discord account before you can join this server.\n\u00a76To link your Minecraft account to Discord, type\:\n\u00a7c{0}\n\u00a76in this server''s Discord server\:\n\u00a7c{1} +discordLinkLoginPrompt=\u00a76You must link your Discord account before you can move, chat on or interact with this server. To link your Minecraft account to Discord, type \u00a7c{0} \u00a76in this server''s Discord server\: \u00a7c{1} +discordLinkNoAccount=\u00a76You do not currently have a Discord account linked to your Minecraft account. +discordLinkPending=\u00a76You already have a link code. To complete linking your Minecraft account to Discord, type \u00a7c{0} \u00a76in the Discord server. +discordLinkUnlinked=\u00a76Unlinked your Minecraft account from all associated discord accounts. discordLoggingIn=Attempting to login to Discord... discordLoggingInDone=Successfully logged in as {0} +discordMailLine=**New mail from {0}:** {1} discordNoSendPermission=Cannot send message in channel: #{0} Please ensure the bot has "Send Messages" permission in that channel\! discordReloadInvalid=Tried to reload EssentialsX Discord config while the plugin is in an invalid state! If you've modified your config, restart your server. disposal=Disposal @@ -661,6 +687,10 @@ lightningCommandUsage2=/ lightningCommandUsage2Description=Strikes lighting at the target player with the given power lightningSmited=\u00a76Thou hast been smitten\! lightningUse=\u00a76Smiting\u00a7c {0} +linkCommandDescription=Generates a code to link your Minecraft account to Discord. +linkCommandUsage=/ +linkCommandUsage1=/ +linkCommandUsage1Description=Generates a code for the /link command on Discord listAfkTag=\u00a77[AFK]\u00a7r listAmount=\u00a76There are \u00a7c{0}\u00a76 out of maximum \u00a7c{1}\u00a76 players online. listAmountHidden=\u00a76There are \u00a7c{0}\u00a76/\u00a7c{1}\u00a76 out of maximum \u00a7c{2}\u00a76 players online. @@ -1028,7 +1058,7 @@ repairCommandUsage2Description=Repairs all items in your inventory repairEnchanted=\u00a74You are not allowed to repair enchanted items. repairInvalidType=\u00a74This item cannot be repaired. repairNone=\u00a74There were no items that needed repairing. -replyFromDiscord=**Reply from {0}\:** `{1}` +replyFromDiscord=**Reply from {0}\:** {1} replyLastRecipientDisabled=\u00a76Replying to last message recipient \u00a7cdisabled\u00a76. replyLastRecipientDisabledFor=\u00a76Replying to last message recipient \u00a7cdisabled \u00a76for \u00a7c{0}\u00a76. replyLastRecipientEnabled=\u00a76Replying to last message recipient \u00a7cenabled\u00a76. @@ -1402,6 +1432,10 @@ unlimitedCommandUsage3=/ clear [player] unlimitedCommandUsage3Description=Clears all unlimited items for yourself or another player if specified unlimitedItemPermission=\u00a74No permission for unlimited item \u00a7c{0}\u00a74. unlimitedItems=\u00a76Unlimited items\:\u00a7r +unlinkCommandDescription=Unlinks your Minecraft account from the currently linked Discord account. +unlinkCommandUsage=/ +unlinkCommandUsage1=/ +unlinkCommandUsage1Description=Unlinks your Minecraft account from the currently linked Discord account. unmutedPlayer=\u00a76Player\u00a7c {0} \u00a76unmuted. unsafeTeleportDestination=\u00a74The teleport destination is unsafe and teleport-safety is disabled. unsupportedBrand=\u00a74The server platform you are currently running does not provide the capabilities for this feature. diff --git a/EssentialsDiscord/src/main/java/net/essentialsx/api/v2/services/discord/DiscordService.java b/EssentialsDiscord/src/main/java/net/essentialsx/api/v2/services/discord/DiscordService.java index 30c3a8dc5..e6a7e5d49 100644 --- a/EssentialsDiscord/src/main/java/net/essentialsx/api/v2/services/discord/DiscordService.java +++ b/EssentialsDiscord/src/main/java/net/essentialsx/api/v2/services/discord/DiscordService.java @@ -4,6 +4,9 @@ import net.essentialsx.api.v2.events.discord.DiscordChatMessageEvent; import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; +import java.util.Collection; +import java.util.concurrent.CompletableFuture; + /** * A class which provides numerous methods to interact with EssentialsX Discord. */ @@ -47,6 +50,34 @@ public interface DiscordService { */ InteractionController getInteractionController(); + /** + * Gets an {@link InteractionMember} by their Discord ID. + * @param id The ID of the member to look up. + * @return A future which will complete with the member or null if none is reachable. + */ + CompletableFuture getMemberById(final String id); + + /** + * Gets an {@link InteractionRole} by its Discord ID. + * @param id The ID of the role to look up. + * @return the role or null if none by that ID exists. + */ + InteractionRole getRole(final String id); + + /** + * Adds or removes {@link InteractionRole roles} to the given {@link InteractionMember}. + * @param member The member to add/remove roles to/from. + * @param addRoles The roles to add to the {@link InteractionMember member}, or null to add none. + * @param removeRoles The roles to remove from the {@link InteractionMember member}, or null to remove none. + * @return A future which will complete when all requests operations have been completed. + */ + CompletableFuture modifyMemberRoles(final InteractionMember member, final Collection addRoles, final Collection removeRoles); + + /** + * Gets the Discord invite URL given in the EssentialsX Discord configuration. + */ + String getInviteUrl(); + /** * Gets unstable API that is subject to change at any time. * @return {@link Unsafe the unsafe} instance. diff --git a/EssentialsDiscord/src/main/java/net/essentialsx/api/v2/services/discord/InteractionCommandArgumentType.java b/EssentialsDiscord/src/main/java/net/essentialsx/api/v2/services/discord/InteractionCommandArgumentType.java index 1e5a17f9f..940abbca2 100644 --- a/EssentialsDiscord/src/main/java/net/essentialsx/api/v2/services/discord/InteractionCommandArgumentType.java +++ b/EssentialsDiscord/src/main/java/net/essentialsx/api/v2/services/discord/InteractionCommandArgumentType.java @@ -4,22 +4,10 @@ package net.essentialsx.api.v2.services.discord; * Represents an argument type to be shown on the Discord client. */ public enum InteractionCommandArgumentType { - STRING(3), - INTEGER(4), - BOOLEAN(5), - USER(6), - CHANNEL(7); - - private final int id; - InteractionCommandArgumentType(int id) { - this.id = id; - } - - /** - * Gets the internal Discord ID for this argument type. - * @return the internal Discord ID. - */ - public int getId() { - return id; - } + STRING, + INTEGER, + BOOLEAN, + USER, + CHANNEL, + ROLE, } diff --git a/EssentialsDiscord/src/main/java/net/essentialsx/api/v2/services/discord/InteractionEvent.java b/EssentialsDiscord/src/main/java/net/essentialsx/api/v2/services/discord/InteractionEvent.java index c4dbdbdc6..eece3cb36 100644 --- a/EssentialsDiscord/src/main/java/net/essentialsx/api/v2/services/discord/InteractionEvent.java +++ b/EssentialsDiscord/src/main/java/net/essentialsx/api/v2/services/discord/InteractionEvent.java @@ -51,6 +51,13 @@ public interface InteractionEvent { */ InteractionChannel getChannelArgument(String key); + /** + * Helper method to get the role representation of the argument by the given key or null if none by that key is present. + * @param key The key of the argument to lookup. + * @return the role value or null + */ + InteractionRole getRoleArgument(String key); + /** * Gets the channel ID where this interaction occurred. * @return the channel ID. diff --git a/EssentialsDiscord/src/main/java/net/essentialsx/api/v2/services/discord/InteractionMember.java b/EssentialsDiscord/src/main/java/net/essentialsx/api/v2/services/discord/InteractionMember.java index afe741487..c6ee1f9bc 100644 --- a/EssentialsDiscord/src/main/java/net/essentialsx/api/v2/services/discord/InteractionMember.java +++ b/EssentialsDiscord/src/main/java/net/essentialsx/api/v2/services/discord/InteractionMember.java @@ -27,6 +27,12 @@ public interface InteractionMember { return getName() + "#" + getDiscriminator(); } + /** + * Gets the discord mention of this member. + * @return this member's mention. + */ + String getAsMention(); + /** * Gets the nickname of this member or their username if they don't have one. * @return this member's nickname or username if none is present. @@ -58,6 +64,20 @@ public interface InteractionMember { */ boolean hasRoles(List roleDefinitions); + /** + * Returns true if the user has the specified {@link InteractionRole role}. + * @param role The role to check for. + * @return true if the member has the specified role. + */ + boolean hasRole(InteractionRole role); + + /** + * Returns true if the user has a role by the specified ID. + * @param roleId The role id to check for. + * @return true if the member has a role by the specified ID. + */ + boolean hasRole(String roleId); + /** * Sends a private message to this member with the given content. * @param content The message to send. diff --git a/EssentialsDiscord/src/main/java/net/essentialsx/api/v2/services/discord/InteractionRole.java b/EssentialsDiscord/src/main/java/net/essentialsx/api/v2/services/discord/InteractionRole.java new file mode 100644 index 000000000..006973463 --- /dev/null +++ b/EssentialsDiscord/src/main/java/net/essentialsx/api/v2/services/discord/InteractionRole.java @@ -0,0 +1,54 @@ +package net.essentialsx.api.v2.services.discord; + +/** + * Represents a role of an interaction member. + */ +public interface InteractionRole { + /** + * Gets the name of this role. + * @return this role's name. + */ + String getName(); + + /** + * Gets the mention of this role. + * @return this role's mention. + */ + String getAsMention(); + + /** + * Whether this role is managed by an external integration. + * @return true if the role is managed. + */ + boolean isManaged(); + + /** + * Whether this role is the default role given to all users (@everyone). + * @return true if this is the default role. + */ + boolean isPublicRole(); + + /** + * Gets the raw RGB color value of this role. + * @return this role's color value. + */ + int getColorRaw(); + + /** + * Whether this role's color is the default one (has no color). + * @return true if the role has no color. + */ + boolean isDefaultColor(); + + /** + * Whether this role can be given to other members by the current logged in bot. + * @return true if this role can be interacted with by the current bot user. + */ + boolean canInteract(); + + /** + * Gets the ID of this role. + * @return this role's ID. + */ + String getId(); +} diff --git a/EssentialsDiscord/src/main/java/net/essentialsx/api/v2/services/discord/MessageType.java b/EssentialsDiscord/src/main/java/net/essentialsx/api/v2/services/discord/MessageType.java index a702101e6..329f40bce 100644 --- a/EssentialsDiscord/src/main/java/net/essentialsx/api/v2/services/discord/MessageType.java +++ b/EssentialsDiscord/src/main/java/net/essentialsx/api/v2/services/discord/MessageType.java @@ -54,6 +54,7 @@ public final class MessageType { */ public static final class DefaultTypes { public final static MessageType JOIN = new MessageType("join", true); + public final static MessageType FIRST_JOIN = new MessageType("first-join", true); public final static MessageType LEAVE = new MessageType("leave", true); public final static MessageType CHAT = new MessageType("chat", true); public final static MessageType DEATH = new MessageType("death", true); @@ -64,7 +65,7 @@ public final class MessageType { public final static MessageType SERVER_STOP = new MessageType("server-stop", false); public final static MessageType KICK = new MessageType("kick", false); public final static MessageType MUTE = new MessageType("mute", false); - private final static MessageType[] VALUES = new MessageType[]{JOIN, LEAVE, CHAT, DEATH, AFK, ADVANCEMENT, ACTION, SERVER_START, SERVER_STOP, KICK, MUTE}; + private final static MessageType[] VALUES = new MessageType[]{JOIN, FIRST_JOIN, LEAVE, CHAT, DEATH, AFK, ADVANCEMENT, ACTION, SERVER_START, SERVER_STOP, KICK, MUTE}; /** * Gets an array of all the default {@link MessageType MessageTypes}. diff --git a/EssentialsDiscord/src/main/java/net/essentialsx/discord/DiscordSettings.java b/EssentialsDiscord/src/main/java/net/essentialsx/discord/DiscordSettings.java index 81a91907d..05ca3533a 100644 --- a/EssentialsDiscord/src/main/java/net/essentialsx/discord/DiscordSettings.java +++ b/EssentialsDiscord/src/main/java/net/essentialsx/discord/DiscordSettings.java @@ -255,6 +255,18 @@ public class DiscordSettings implements IConf { "username", "displayname", "joinmessage", "online", "unique"); } + public MessageFormat getFirstJoinFormat(Player player) { + final String format = getFormatString("first-join"); + final String filled; + if (plugin.isPAPI() && format != null) { + filled = me.clip.placeholderapi.PlaceholderAPI.setPlaceholders(player, format); + } else { + filled = format; + } + return generateMessageFormat(filled, ":arrow_right: :first_place: {displayname} has joined the server for the first time!", false, + "username", "displayname", "joinmessage", "online", "unique"); + } + public MessageFormat getQuitFormat(Player player) { final String format = getFormatString("quit"); final String filled; diff --git a/EssentialsDiscord/src/main/java/net/essentialsx/discord/JDADiscordService.java b/EssentialsDiscord/src/main/java/net/essentialsx/discord/JDADiscordService.java index a9057d9f6..9ce07b6fe 100644 --- a/EssentialsDiscord/src/main/java/net/essentialsx/discord/JDADiscordService.java +++ b/EssentialsDiscord/src/main/java/net/essentialsx/discord/JDADiscordService.java @@ -13,6 +13,7 @@ import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.JDABuilder; import net.dv8tion.jda.api.entities.Emote; import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Role; import net.dv8tion.jda.api.entities.TextChannel; import net.dv8tion.jda.api.entities.Webhook; import net.dv8tion.jda.api.events.ShutdownEvent; @@ -26,9 +27,13 @@ import net.essentialsx.api.v2.events.discord.DiscordMessageEvent; import net.essentialsx.api.v2.services.discord.DiscordService; import net.essentialsx.api.v2.services.discord.InteractionController; import net.essentialsx.api.v2.services.discord.InteractionException; +import net.essentialsx.api.v2.services.discord.InteractionMember; +import net.essentialsx.api.v2.services.discord.InteractionRole; import net.essentialsx.api.v2.services.discord.MessageType; import net.essentialsx.api.v2.services.discord.Unsafe; import net.essentialsx.discord.interactions.InteractionControllerImpl; +import net.essentialsx.discord.interactions.InteractionMemberImpl; +import net.essentialsx.discord.interactions.InteractionRoleImpl; import net.essentialsx.discord.interactions.commands.ExecuteCommand; import net.essentialsx.discord.interactions.commands.ListCommand; import net.essentialsx.discord.interactions.commands.MessageCommand; @@ -46,7 +51,10 @@ import org.bukkit.plugin.ServicePriority; import org.jetbrains.annotations.NotNull; import javax.security.auth.login.LoginException; +import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; @@ -333,7 +341,7 @@ public class JDADiscordService implements DiscordService, IEssentialsModule { if (current != null) { current.close(); } - channelIdToWebhook.remove(channel.getId()); + channelIdToWebhook.remove(channel.getId()).close(); continue; } typeToChannelId.put(type, channel.getId()); @@ -392,7 +400,7 @@ public class JDADiscordService implements DiscordService, IEssentialsModule { shutdownConsoleRelay(false); consoleWebhook = DiscordUtil.getWebhookClient(webhookId, webhookToken, jda.getHttpClient()); - if (injector == null) { + if (injector == null || injector.isRemoved()) { injector = new ConsoleInjector(this); injector.start(); } @@ -430,6 +438,10 @@ public class JDADiscordService implements DiscordService, IEssentialsModule { shutdownConsoleRelay(true); + for (WebhookClient webhook : channelIdToWebhook.values()) { + webhook.close(); + } + // Unregister leftover jda listeners for (Object obj : jda.getRegisteredListeners()) { if (!(obj instanceof EventListener)) { // Yeah bro I wish I knew too :/ @@ -462,6 +474,54 @@ public class JDADiscordService implements DiscordService, IEssentialsModule { } } + @Override + public CompletableFuture getMemberById(final String id) { + final CompletableFuture future = new CompletableFuture<>(); + getGuild().retrieveMemberById(id).queue(member -> { + if (member != null) { + future.complete(new InteractionMemberImpl(member)); + return; + } + future.complete(null); + }, fail -> future.complete(null)); + return future; + } + + @Override + public InteractionRole getRole(String id) { + final Role role = getGuild().getRoleById(id); + return role == null ? null : new InteractionRoleImpl(role); + } + + @Override + public CompletableFuture modifyMemberRoles(InteractionMember member, Collection addRoles, Collection removeRoles) { + if ((addRoles == null || addRoles.isEmpty()) && (removeRoles == null || removeRoles.isEmpty())) { + return CompletableFuture.completedFuture(null); + } + + final List add = new ArrayList<>(); + final List remove = new ArrayList<>(); + if (addRoles != null) { + for (final InteractionRole role : addRoles) { + add.add(((InteractionRoleImpl) role).getJdaObject()); + } + } + if (removeRoles != null) { + for (final InteractionRole role : removeRoles) { + remove.add(((InteractionRoleImpl) role).getJdaObject()); + } + } + + final CompletableFuture future = new CompletableFuture<>(); + guild.modifyMemberRoles(((InteractionMemberImpl) member).getJdaObject(), add, remove).queue(future::complete); + return future; + } + + @Override + public String getInviteUrl() { + return getSettings().getDiscordUrl(); + } + public JDA getJda() { return jda; } diff --git a/EssentialsDiscord/src/main/java/net/essentialsx/discord/interactions/InteractionEventImpl.java b/EssentialsDiscord/src/main/java/net/essentialsx/discord/interactions/InteractionEventImpl.java index c6c601934..b43030d04 100644 --- a/EssentialsDiscord/src/main/java/net/essentialsx/discord/interactions/InteractionEventImpl.java +++ b/EssentialsDiscord/src/main/java/net/essentialsx/discord/interactions/InteractionEventImpl.java @@ -9,6 +9,7 @@ import net.dv8tion.jda.api.interactions.commands.OptionMapping; import net.essentialsx.api.v2.services.discord.InteractionChannel; import net.essentialsx.api.v2.services.discord.InteractionEvent; import net.essentialsx.api.v2.services.discord.InteractionMember; +import net.essentialsx.api.v2.services.discord.InteractionRole; import net.essentialsx.discord.EssentialsDiscord; import net.essentialsx.discord.util.DiscordUtil; @@ -79,6 +80,12 @@ public class InteractionEventImpl implements InteractionEvent { return mapping == null ? null : new InteractionChannelImpl(mapping.getAsGuildChannel()); } + @Override + public InteractionRole getRoleArgument(String key) { + final OptionMapping mapping = event.getOption(key); + return mapping == null ? null : new InteractionRoleImpl(mapping.getAsRole()); + } + @Override public String getChannelId() { return event.getChannel().getId(); diff --git a/EssentialsDiscord/src/main/java/net/essentialsx/discord/interactions/InteractionMemberImpl.java b/EssentialsDiscord/src/main/java/net/essentialsx/discord/interactions/InteractionMemberImpl.java index 1184fdba8..9abfb994c 100644 --- a/EssentialsDiscord/src/main/java/net/essentialsx/discord/interactions/InteractionMemberImpl.java +++ b/EssentialsDiscord/src/main/java/net/essentialsx/discord/interactions/InteractionMemberImpl.java @@ -3,7 +3,9 @@ package net.essentialsx.discord.interactions; import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.PrivateChannel; +import net.dv8tion.jda.api.entities.Role; import net.essentialsx.api.v2.services.discord.InteractionMember; +import net.essentialsx.api.v2.services.discord.InteractionRole; import net.essentialsx.discord.util.DiscordUtil; import java.util.List; @@ -26,6 +28,11 @@ public class InteractionMemberImpl implements InteractionMember { return member.getUser().getDiscriminator(); } + @Override + public String getAsMention() { + return member.getAsMention(); + } + @Override public String getEffectiveName() { return member.getEffectiveName(); @@ -51,6 +58,21 @@ public class InteractionMemberImpl implements InteractionMember { return DiscordUtil.hasRoles(member, roleDefinitions); } + @Override + public boolean hasRole(InteractionRole role) { + return hasRole(role.getId()); + } + + @Override + public boolean hasRole(String roleId) { + for (final Role role : member.getRoles()) { + if (role.getId().equals(roleId)) { + return true; + } + } + return false; + } + public Member getJdaObject() { return member; } diff --git a/EssentialsDiscord/src/main/java/net/essentialsx/discord/interactions/InteractionRoleImpl.java b/EssentialsDiscord/src/main/java/net/essentialsx/discord/interactions/InteractionRoleImpl.java new file mode 100644 index 000000000..545caad5e --- /dev/null +++ b/EssentialsDiscord/src/main/java/net/essentialsx/discord/interactions/InteractionRoleImpl.java @@ -0,0 +1,61 @@ +package net.essentialsx.discord.interactions; + +import net.dv8tion.jda.api.entities.Role; +import net.essentialsx.api.v2.services.discord.InteractionRole; + +public class InteractionRoleImpl implements InteractionRole { + private final Role role; + + public InteractionRoleImpl(Role role) { + this.role = role; + } + + @Override + public String getName() { + return role.getName(); + } + + @Override + public String getAsMention() { + return role.getAsMention(); + } + + @Override + public boolean isManaged() { + return role.isManaged(); + } + + @Override + public boolean isPublicRole() { + return role.isPublicRole(); + } + + @Override + public int getColorRaw() { + return role.getColorRaw(); + } + + @Override + public boolean isDefaultColor() { + return role.getColorRaw() == Role.DEFAULT_COLOR_RAW; + } + + @Override + public boolean canInteract() { + return role.getGuild().getSelfMember().canInteract(role); + } + + public Role getJdaObject() { + return role; + } + + @Override + public String getId() { + return role.getId(); + } + + @Override + public String toString() { + return "InteractionRoleImpl{name=" + getName() + ",id=" + getId() + "}"; + } +} diff --git a/EssentialsDiscord/src/main/java/net/essentialsx/discord/listeners/BukkitListener.java b/EssentialsDiscord/src/main/java/net/essentialsx/discord/listeners/BukkitListener.java index 3a5cf72f7..79d3130ea 100644 --- a/EssentialsDiscord/src/main/java/net/essentialsx/discord/listeners/BukkitListener.java +++ b/EssentialsDiscord/src/main/java/net/essentialsx/discord/listeners/BukkitListener.java @@ -99,14 +99,19 @@ public class BukkitListener implements Listener { public void onJoin(AsyncUserDataLoadEvent event) { // Delay join to let nickname load if (!isSilentJoinQuit(event.getUser(), "join") && !isVanishHide(event.getUser())) { - sendJoinQuitMessage(event.getUser().getBase(), event.getJoinMessage(), true); + // Check if this is the first time the player has joined + if (!event.getUser().getBase().hasPlayedBefore()) { + sendJoinQuitMessage(event.getUser().getBase(), event.getJoinMessage(), MessageType.DefaultTypes.FIRST_JOIN); + } else { + sendJoinQuitMessage(event.getUser().getBase(), event.getJoinMessage(), MessageType.DefaultTypes.JOIN); + } } } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onQuit(PlayerQuitEvent event) { if (!isSilentJoinQuit(event.getPlayer(), "quit") && !isVanishHide(event.getPlayer())) { - sendJoinQuitMessage(event.getPlayer(), event.getQuitMessage(), false); + sendJoinQuitMessage(event.getPlayer(), event.getQuitMessage(), MessageType.DefaultTypes.LEAVE); } } @@ -124,20 +129,35 @@ public class BukkitListener implements Listener { return; } if (event.getValue()) { - sendJoinQuitMessage(event.getAffected().getBase(), ChatColor.YELLOW + event.getAffected().getName() + " left the game", false); + sendJoinQuitMessage(event.getAffected().getBase(), ChatColor.YELLOW + event.getAffected().getName() + " left the game", MessageType.DefaultTypes.LEAVE); return; } - sendJoinQuitMessage(event.getAffected().getBase(), ChatColor.YELLOW + event.getAffected().getName() + " joined the game", true); + sendJoinQuitMessage(event.getAffected().getBase(), ChatColor.YELLOW + event.getAffected().getName() + " joined the game", MessageType.DefaultTypes.JOIN); } - public void sendJoinQuitMessage(final Player player, final String message, boolean join) { - sendDiscordMessage(join ? MessageType.DefaultTypes.JOIN : MessageType.DefaultTypes.LEAVE, - MessageUtil.formatMessage(join ? jda.getSettings().getJoinFormat(player) : jda.getSettings().getQuitFormat(player), + public void sendJoinQuitMessage(final Player player, final String message, MessageType type) { + int userCount = jda.getPlugin().getEss().getUsers().getUserCount(); + final MessageFormat format; + switch (type.getKey()) { + case "join": + format = jda.getSettings().getJoinFormat(player); + break; + case "first-join": + format = jda.getSettings().getFirstJoinFormat(player); + break; + default: // So that it will always be initialised. Other options shouldn't be possible. + format = jda.getSettings().getQuitFormat(player); + userCount = userCount - 1; + break; + + } + sendDiscordMessage(type, + MessageUtil.formatMessage(format, MessageUtil.sanitizeDiscordMarkdown(player.getName()), MessageUtil.sanitizeDiscordMarkdown(player.getDisplayName()), MessageUtil.sanitizeDiscordMarkdown(message), - jda.getPlugin().getEss().getOnlinePlayers().size() - (join ? 0 : 1), - jda.getPlugin().getEss().getUsers().getUserCount()), + jda.getPlugin().getEss().getOnlinePlayers().size(), + userCount), player); } diff --git a/EssentialsDiscord/src/main/java/net/essentialsx/discord/util/ConsoleInjector.java b/EssentialsDiscord/src/main/java/net/essentialsx/discord/util/ConsoleInjector.java index 977efb37b..3d2502770 100644 --- a/EssentialsDiscord/src/main/java/net/essentialsx/discord/util/ConsoleInjector.java +++ b/EssentialsDiscord/src/main/java/net/essentialsx/discord/util/ConsoleInjector.java @@ -27,6 +27,7 @@ public class ConsoleInjector extends AbstractAppender { private final JDADiscordService jda; private final BlockingQueue messageQueue = new LinkedBlockingQueue<>(); private final int taskId; + private boolean removed = false; public ConsoleInjector(JDADiscordService jda) { super("EssentialsX-ConsoleInjector", null, null, false); @@ -93,5 +94,10 @@ public class ConsoleInjector extends AbstractAppender { ((Logger) LogManager.getRootLogger()).removeAppender(this); Bukkit.getScheduler().cancelTask(taskId); messageQueue.clear(); + removed = true; + } + + public boolean isRemoved() { + return removed; } } diff --git a/EssentialsDiscord/src/main/resources/config.yml b/EssentialsDiscord/src/main/resources/config.yml index 1c3072b87..e87149d0e 100644 --- a/EssentialsDiscord/src/main/resources/config.yml +++ b/EssentialsDiscord/src/main/resources/config.yml @@ -116,6 +116,8 @@ console: message-types: # Join messages sent when a player joins the Minecraft server. join: primary + # Join messages sent when a player joins the Minecraft server for the first time. This type is sent instead of the join type. + first-join: primary # Leave messages sent when a player leaves the Minecraft server. leave: primary # Chat messages sent when a player chats on the Minecraft server. @@ -307,6 +309,15 @@ messages: # - {unique}: The amount of unique players to ever join the server # ... PlaceholderAPI placeholders are also supported here too! join: ":arrow_right: {displayname} has joined!" + # This is the message sent to Discord when a player joins the minecraft server for the first time. + # The following placeholders can be used here: + # - {username}: The name of the user joining + # - {displayname}: The display name of the user joining + # - {joinmessage}: The full default join message used in game + # - {online}: The amount of players online + # - {unique}: The amount of unique players to ever join the server + # ... PlaceholderAPI placeholders are also supported here too! + first-join: ":arrow_right: :first_place: {displayname} has joined the server for the first time!" # This is the message sent to Discord when a player leaves the minecraft server. # The following placeholders can be used here: # - {username}: The name of the user leaving diff --git a/EssentialsDiscordLink/README.md b/EssentialsDiscordLink/README.md new file mode 100644 index 000000000..af8c3d758 --- /dev/null +++ b/EssentialsDiscordLink/README.md @@ -0,0 +1,102 @@ +# EssentialsX Discord Link + +EssentialsX Discord Link is an addon for EssentialsX Discord which provides numerous features related to +group/role synchronization. + +EssentialsX Discord Link offers features such as: +* Vault Group -> Discord Role Synchronization +* Discord Role -> Vault Group Synchronization +* Prevent unlinked players from joining +* Prevent unlinked players from moving/chatting +* & more... + +--- + +## Table of Contents +> * [Setting Up Role Sync](#setting-up-role-sync) +> * [Linking an Account](#linking-an-account) +> * [Developer API](#developer-api) + +--- + +## Setting Up Role Sync + +In EssentialsX Discord Link, you can define a synchronizations for both Vault groups -> Discord roles and for +Discord roles -> Vault groups. + +The following tutorial (as an example) will show how to give players with the Discord role `Patreon` the `donator` +Vault group and how to give players with the `vip` Vault group the `VIP` Discord role. + +0. First, head to your server's role page in order to get their IDs. + +1. For both the `Patreon` and `VIP` role, right click them and click on "Copy ID". +> ![Copy Role ID](https://i.imgur.com/YS9P2ej.gif) +> Right Click on Role(s) -> `Copy ID` -> Paste into Notepad for later step + +2. Now that you have the IDs you need from Discord, you can begin configuring the plugin. First place the +EssentialsX Discord Link jar (you can download it [here](https://essentialsx.net/downloads.html) if you do not +already have it) in your plugins folder and then start your server. +> ![Start Server](https://i.imgur.com/64IwqoO.gif) +> Drag EssentialsXDiscordLink jar into plugins folder -> Start Server + +3. Once the server started, open the config for EssentialsX Discord Link at +`plugins/EssentialsDiscordLink/config.yml`. Once opened, put `group-name: role-id` in the `groups` section +to create a Vault group -> Discord role synchronization (`vip: 882835722640433242` for this example); Then put +`role-id: group-name` in the `roles` section to create a Discord role -> Vault group synchronization +(`882835662280224818: donator` for this example). When done, save the file. +> ![Paste Synchronizations](https://i.imgur.com/JYZHzW0.gif) +> Paste Vault->Discord syncs in the group section & Discord->Vault syncs in the roles section + +5. Finally, once the file is saved, run `ess reload` from your console and then linked accounts should now have +their groups/roles linked between Minecraft/Discord! Now that you completed the basics of group/role syncing, +go back up to the [Table of Contents](#table-of-contents) to see what else you can do! + +--- + +## Linking an Account + +0. This assumes the server has started and you have joined the server. + +1. Once on the server, run `/link` in Minecraft and take note of the code if gives you. +> ![Run /link](https://i.imgur.com/1EdqdOa.gif) +> Run `/link` in Minecraft + +2. Next, all you have to do is run the `/link` command in discord with the code provided. +> ![Run /link in Discord](https://i.imgur.com/yXkvMDX.gif) +> Run `/link` with the code in Discord + +3. That's it! Now that you've learned how to link an account, go back up to the +[Table of Contents](#table-of-contents) to see what else you can do! + +--- + +## Developer API + +EssentialsX Discord Link has a simple API to provide very simple methods to check if players are linked, +link players, unlink players, and to get linked player data. + +Outside the specific examples below, you can also view javadocs for EssentialsX Discord Link +[here](https://jd-v2.essentialsx.net/EssentialsDiscordLink). + +### Get a linked player's Discord tag + +The following example shows how to get a linked player's Discord tag (in `Name#0000` format) or null if the player +isn't linked. + +```java +public String getDiscordTag(final Player player) { + // Gets the API service for EssentialsX Discord Link + final DiscordLinkService linkApi = Bukkit.getServicesManager().load(DiscordLinkService.class); + + final String discordId = linkApi.getDiscordId(player.getUniqueId()); + if (discordId == null) { + return null; + } + + // Gets the API service for EssentialsX Discord which we will use to get the actual user + final DiscordService discordApi = Bukkit.getServicesManager().load(DiscordService.class); + + final InteractionMember member = discordApi.getMemberById(discordId).join(); + return member == null ? null : member.getTag(); +} +``` \ No newline at end of file diff --git a/EssentialsDiscordLink/build.gradle b/EssentialsDiscordLink/build.gradle new file mode 100644 index 000000000..542f00470 --- /dev/null +++ b/EssentialsDiscordLink/build.gradle @@ -0,0 +1,8 @@ +plugins { + id("essentials.module-conventions") +} + +dependencies { + compileOnly project(':EssentialsX') + compileOnly project(':EssentialsXDiscord') +} diff --git a/EssentialsDiscordLink/src/main/java/net/essentialsx/api/v2/events/discordlink/DiscordLinkStatusChangeEvent.java b/EssentialsDiscordLink/src/main/java/net/essentialsx/api/v2/events/discordlink/DiscordLinkStatusChangeEvent.java new file mode 100644 index 000000000..551b18abd --- /dev/null +++ b/EssentialsDiscordLink/src/main/java/net/essentialsx/api/v2/events/discordlink/DiscordLinkStatusChangeEvent.java @@ -0,0 +1,109 @@ +package net.essentialsx.api.v2.events.discordlink; + +import net.ess3.api.IUser; +import net.essentialsx.api.v2.services.discord.InteractionMember; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; + +/** + * Fired when a User's link status has changed. + */ +public class DiscordLinkStatusChangeEvent extends Event { + private static final HandlerList handlers = new HandlerList(); + + private final IUser user; + private final InteractionMember member; + private final String memberId; + private final boolean state; + private final Cause cause; + + public DiscordLinkStatusChangeEvent(IUser user, InteractionMember member, String memberId, boolean state, Cause cause) { + this.user = user; + this.member = member; + this.memberId = memberId; + this.state = state; + this.cause = cause; + } + + /** + * Gets the Essentials {@link IUser user} whose link status has been changed in this event. + * @return the user. + */ + public IUser getUser() { + return user; + } + + /** + * Gets the Discord {@link InteractionMember member} whose link status has been changed in this event. + *

+ * This will return {@code null} if {@link #getCause()} returns {@link Cause#UNSYNC_LEAVE}. + * @see #getCause() + * @see #getMemberId() + * @return the member or null. + */ + public InteractionMember getMember() { + return member; + } + + /** + * Gets the ID of the Discord member whose link status has been changed in this event. + *

+ * Unlink {@link #getMember()}, this method will never return null. + * @return the member's id. + */ + public String getMemberId() { + return memberId; + } + + /** + * Gets the new link status of this {@link #getUser() user} after this event. + * @return true if the user is linked to a discord account. + */ + public boolean isLinked() { + return state; + } + + /** + * The cause which triggered this event. + * @see Cause + * @return the cause. + */ + public Cause getCause() { + return cause; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } + + /** + * The cause of the link status change. + */ + public enum Cause { + /** + * Used when a player successfully completes an account link with the /link account in Minecraft. + */ + SYNC_PLAYER, + /** + * Used when a player is linked via an external plugin using API. + */ + SYNC_API, + /** + * Used when a player unlinks their account via the /unlink Discord or Minecraft command. + */ + UNSYNC_PLAYER, + /** + * Used when a player is unlinked via an external plugin using API. + */ + UNSYNC_API, + /** + * Used when a player is unlinked due to them leaving the Discord server. + */ + UNSYNC_LEAVE, + } +} diff --git a/EssentialsDiscordLink/src/main/java/net/essentialsx/api/v2/services/discordlink/DiscordLinkService.java b/EssentialsDiscordLink/src/main/java/net/essentialsx/api/v2/services/discordlink/DiscordLinkService.java new file mode 100644 index 000000000..8cf09a0bc --- /dev/null +++ b/EssentialsDiscordLink/src/main/java/net/essentialsx/api/v2/services/discordlink/DiscordLinkService.java @@ -0,0 +1,85 @@ +package net.essentialsx.api.v2.services.discordlink; + +import net.essentialsx.api.v2.services.discord.InteractionMember; + +import java.util.UUID; + +/** + * A class which provides numerous methods to interact with the link module for EssentialsX Discord. + */ +public interface DiscordLinkService { + /** + * Gets the Discord ID linked to the given {@link UUID} or {@code null} if none is present. + * @param uuid the {@link UUID} of the player to lookup. + * @return the Discord ID or {@code null}. + */ + String getDiscordId(final UUID uuid); + + /** + * Checks if there is a Discord account linked to the given {@link UUID}. + * @param uuid the {@link UUID} to check. + * @return true if there is a Discord account linked to the given {@link UUID}. + */ + default boolean isLinked(final UUID uuid) { + return getDiscordId(uuid) != null; + } + + /** + * Gets the {@link UUID} linked to the given Discord ID or {@code null} if none is present. + * @param discordId The Discord ID to lookup. + * @return the {@link UUID} or {@code null}. + */ + UUID getUUID(final String discordId); + + /** + * Checks if there is a Minecraft account linked to the given Discord ID. + * @param discordId the Discord ID to check. + * @return true if there is a Minecraft account linked to the given Discord ID. + */ + default boolean isLinked(final String discordId) { + return getUUID(discordId) != null; + } + + /** + * Links the given {@link UUID} to the given {@link InteractionMember}. + *

+ * This will automatically trigger role sync (if configured) for the given + * player if this method returns {@code true}. + *

+ * This method will return true if the accounts are successfully linked, or + * false if either the provided {@link UUID} or {@link InteractionMember} are + * already linked to another account. + * @param uuid The {@link UUID} of the target player. + * @param member The {@link InteractionMember} to link to the target player. + * @see net.essentialsx.api.v2.services.discord.DiscordService#getMemberById(String) to get an + * {@link InteractionMember} by their ID. + * @see #isLinked(UUID) to ensure the given {@link UUID} isn't already linked to an account. + * @see #isLinked(String) to ensure the given {@link InteractionMember} isn't already linked to an account. + * @throws IllegalArgumentException if either of the {@link UUID} or {@link InteractionMember} are null. + * @return true if the accounts were linked successfully, otherwise false. + */ + boolean linkAccount(final UUID uuid, final InteractionMember member); + + /** + * Unlinks the given {@link UUID} with its associated Discord account (if present). + *

+ * This will automatically trigger role unsync (if configured) for the given player if this method + * returns {@code true}. + * @param uuid The {@link UUID} of the player to unlink. + * @throws IllegalArgumentException if the provided {@link UUID} is null. + * @return true if there was an account associated with the given {@link UUID}, otherwise false. + */ + boolean unlinkAccount(final UUID uuid); + + /** + * Unlinks the given {@link InteractionMember} with its associated Minecraft account (if present). + *

+ * This will automatically trigger role unsync (if configured) for the given {@link InteractionMember} + * if this method returns {@code true}. + * @param member The {@link InteractionMember} to unlink. + * @throws IllegalArgumentException if the provided {@link InteractionMember} is null. + * @return true if there was a linked Minecraft account associated with the given + * {@link InteractionMember}, otherwise false. + */ + boolean unlinkAccount(final InteractionMember member); +} diff --git a/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/AccountLinkManager.java b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/AccountLinkManager.java new file mode 100644 index 000000000..10b33d9d5 --- /dev/null +++ b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/AccountLinkManager.java @@ -0,0 +1,168 @@ +package net.essentialsx.discordlink; + +import com.earth2me.essentials.IEssentialsModule; +import com.google.common.base.Preconditions; +import net.ess3.api.IUser; +import net.essentialsx.api.v2.events.discordlink.DiscordLinkStatusChangeEvent; +import net.essentialsx.api.v2.services.discord.InteractionMember; +import net.essentialsx.api.v2.services.discordlink.DiscordLinkService; +import net.essentialsx.discordlink.rolesync.RoleSyncManager; + +import java.util.Map; +import java.util.Optional; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ThreadLocalRandom; + +public class AccountLinkManager implements IEssentialsModule, DiscordLinkService { + private static final char[] CODE_CHARACTERS = "abcdefghijklmnopqrstuvwxyz0123456789".toCharArray(); + + private final EssentialsDiscordLink ess; + private final AccountStorage storage; + private final RoleSyncManager roleSyncManager; + + private final Map codeToUuidMap = new ConcurrentHashMap<>(); + + public AccountLinkManager(EssentialsDiscordLink ess, AccountStorage storage, RoleSyncManager roleSyncManager) { + this.ess = ess; + this.storage = storage; + this.roleSyncManager = roleSyncManager; + } + + public String createCode(final UUID uuid) throws IllegalArgumentException { + final Optional> prevCode = codeToUuidMap.entrySet().stream().filter(stringUUIDEntry -> stringUUIDEntry.getValue().equals(uuid)).findFirst(); + if (prevCode.isPresent()) { + throw new IllegalArgumentException(prevCode.get().getKey()); + } + + final String code = generateCode(); + + codeToUuidMap.put(code, uuid); + return code; + } + + public UUID getPendingUUID(final String code) { + return codeToUuidMap.remove(code); + } + + @Override + public String getDiscordId(final UUID uuid) { + return storage.getDiscordId(uuid); + } + + public IUser getUser(final String discordId) { + final UUID uuid = getUUID(discordId); + if (uuid == null) { + return null; + } + return ess.getEss().getUser(uuid); + } + + @Override + public UUID getUUID(final String discordId) { + return storage.getUUID(discordId); + } + + @Override + public boolean unlinkAccount(InteractionMember member) { + Preconditions.checkNotNull(member, "member cannot be null"); + + if (!isLinked(member.getId())) { + return false; + } + + removeAccount(member, DiscordLinkStatusChangeEvent.Cause.UNSYNC_API); + return true; + } + + public boolean removeAccount(final InteractionMember member, final DiscordLinkStatusChangeEvent.Cause cause) { + final UUID uuid = getUUID(member.getId()); + if (storage.remove(member.getId())) { + ensureAsync(() -> { + final IUser user = ess.getEss().getUser(uuid); + ensureSync(() -> ess.getServer().getPluginManager().callEvent(new DiscordLinkStatusChangeEvent(user, member, member.getId(), false, cause))); + }); + return true; + } + ensureAsync(() -> roleSyncManager.unSync(uuid, member.getId())); + return false; + } + + @Override + public boolean unlinkAccount(UUID uuid) { + Preconditions.checkNotNull(uuid, "uuid cannot be null"); + + if (!isLinked(uuid)) { + return false; + } + + ensureAsync(() -> removeAccount(ess.getEss().getUser(uuid), DiscordLinkStatusChangeEvent.Cause.UNSYNC_API)); + return true; + } + + public boolean removeAccount(final IUser user, final DiscordLinkStatusChangeEvent.Cause cause) { + final String id = getDiscordId(user.getBase().getUniqueId()); + if (storage.remove(user.getBase().getUniqueId())) { + ess.getApi().getMemberById(id).thenAccept(member -> ensureSync(() -> + ess.getServer().getPluginManager().callEvent(new DiscordLinkStatusChangeEvent(user, member, id, false, cause)))); + return true; + } + ensureAsync(() -> roleSyncManager.unSync(user.getBase().getUniqueId(), id)); + return false; + } + + @Override + public boolean linkAccount(UUID uuid, InteractionMember member) { + Preconditions.checkNotNull(uuid, "uuid cannot be null"); + Preconditions.checkNotNull(member, "member cannot be null"); + + if (isLinked(uuid) || isLinked(member.getId())) { + return false; + } + + registerAccount(uuid, member, DiscordLinkStatusChangeEvent.Cause.SYNC_API); + return true; + } + + public void registerAccount(final UUID uuid, final InteractionMember member, final DiscordLinkStatusChangeEvent.Cause cause) { + storage.add(uuid, member.getId()); + ensureAsync(() -> roleSyncManager.sync(uuid, member.getId())); + ensureAsync(() -> { + final IUser user = ess.getEss().getUser(uuid); + ensureSync(() -> ess.getServer().getPluginManager().callEvent(new DiscordLinkStatusChangeEvent(user, member, member.getId(), true, cause))); + }); + } + + private void ensureSync(final Runnable runnable) { + if (ess.getServer().isPrimaryThread()) { + runnable.run(); + return; + } + ess.getEss().scheduleSyncDelayedTask(runnable); + } + + private void ensureAsync(final Runnable runnable) { + if (!ess.getServer().isPrimaryThread()) { + runnable.run(); + return; + } + ess.getEss().runTaskAsynchronously(runnable); + } + + private String generateCode() { + final char[] code = new char[8]; + final Random random = ThreadLocalRandom.current(); + + for (int i = 0; i < 8; i++) { + code[i] = CODE_CHARACTERS[random.nextInt(CODE_CHARACTERS.length)]; + } + final String result = new String(code); + + if (codeToUuidMap.containsKey(result)) { + // If this happens, buy a lottery ticket. + return generateCode(); + } + return result; + } +} diff --git a/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/AccountStorage.java b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/AccountStorage.java new file mode 100644 index 000000000..68bf0552a --- /dev/null +++ b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/AccountStorage.java @@ -0,0 +1,118 @@ +package net.essentialsx.discordlink; + +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; +import com.google.common.collect.Maps; +import com.google.common.reflect.TypeToken; +import com.google.gson.Gson; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; + +public class AccountStorage { + private final Gson gson = new Gson(); + private final EssentialsDiscordLink plugin; + private final File accountFile; + private final BiMap uuidToDiscordIdMap; + private final AtomicBoolean mapDirty = new AtomicBoolean(false); + private final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); + + public AccountStorage(final EssentialsDiscordLink plugin) throws IOException { + this.plugin = plugin; + this.accountFile = new File(plugin.getDataFolder(), "accounts.json"); + if (!plugin.getDataFolder().exists() && !plugin.getDataFolder().mkdirs()) { + throw new IOException("Unable to create account file!"); + } + if (!accountFile.exists() && !accountFile.createNewFile()) { + throw new IOException("Unable to create account file!"); + } + try (final Reader reader = new FileReader(accountFile)) { + //noinspection UnstableApiUsage + final Map map = gson.fromJson(reader, new TypeToken>() {}.getType()); + uuidToDiscordIdMap = map == null ? Maps.synchronizedBiMap(HashBiMap.create()) : Maps.synchronizedBiMap(HashBiMap.create(map)); + } + + executorService.scheduleWithFixedDelay(() -> { + if (!mapDirty.compareAndSet(true, false)) { + return; + } + + if (plugin.getEss().getSettings().isDebug()) { + plugin.getLogger().log(Level.INFO, "Saving linked discord accounts to disk..."); + } + + final Map clone; + clone = new HashMap<>(uuidToDiscordIdMap); + try (final Writer writer = new FileWriter(accountFile)) { + gson.toJson(clone, writer); + } catch (IOException e) { + plugin.getLogger().log(Level.SEVERE, "Failed to save link accounts!", e); + mapDirty.set(true); // mark the map as dirty and pray it fixes itself :D + } + }, 10, 10, TimeUnit.SECONDS); + } + + public BiMap getRawStorageMap() { + return HashBiMap.create(uuidToDiscordIdMap); + } + + public void add(final UUID uuid, final String discordId) { + uuidToDiscordIdMap.forcePut(uuid.toString(), discordId); + queueSave(); + } + + public boolean remove(final UUID uuid) { + final boolean success = uuidToDiscordIdMap.remove(uuid.toString()) != null; + queueSave(); + return success; + } + + public boolean remove(final String discordId) { + final boolean success = uuidToDiscordIdMap.values().removeIf(discordId::equals); + queueSave(); + return success; + } + + public UUID getUUID(final String discordId) { + final String uuid = uuidToDiscordIdMap.inverse().get(discordId); + return uuid == null ? null : UUID.fromString(uuid); + } + + public String getDiscordId(final UUID uuid) { + return uuidToDiscordIdMap.get(uuid.toString()); + } + + public void queueSave() { + mapDirty.set(true); + } + + public void shutdown() { + try { + executorService.shutdown(); + if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) { + plugin.getLogger().log(Level.SEVERE, "Timed out while saving!"); + executorService.shutdownNow(); + } + if (mapDirty.get()) { + try (final Writer writer = new FileWriter(accountFile)) { + gson.toJson(uuidToDiscordIdMap, writer); + } + } + } catch (InterruptedException | IOException e) { + plugin.getLogger().log(Level.SEVERE, "Failed to shutdown link accounts save!", e); + executorService.shutdownNow(); + } + } +} diff --git a/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/DiscordLinkSettings.java b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/DiscordLinkSettings.java new file mode 100644 index 000000000..ad5b668af --- /dev/null +++ b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/DiscordLinkSettings.java @@ -0,0 +1,96 @@ +package net.essentialsx.discordlink; + +import com.earth2me.essentials.IConf; +import com.earth2me.essentials.config.EssentialsConfiguration; + +import java.io.File; +import java.util.Map; + +public class DiscordLinkSettings implements IConf { + private final EssentialsDiscordLink plugin; + private final EssentialsConfiguration config; + + private LinkPolicy linkPolicy; + private Map roleSyncGroups; + private Map roleSyncRoles; + + public DiscordLinkSettings(EssentialsDiscordLink plugin) { + this.plugin = plugin; + this.config = new EssentialsConfiguration(new File(plugin.getDataFolder(), "config.yml"), "/config.yml", EssentialsDiscordLink.class); + reloadConfig(); + } + + public LinkPolicy getLinkPolicy() { + return linkPolicy; + } + + public boolean isBlockUnlinkedChat() { + return config.getBoolean("block-unlinked-chat", false); + } + + public boolean isUnlinkOnLeave() { + return config.getBoolean("unlink-on-leave", true); + } + + public boolean isRelayMail() { + return config.getBoolean("relay-mail", true); + } + + public boolean isRoleSyncRemoveRoles() { + return config.getBoolean("role-sync.remove-roles", true); + } + + public boolean isRoleSyncRemoveGroups() { + return config.getBoolean("role-sync.remove-groups", true); + } + + public int getRoleSyncResyncDelay() { + return config.getInt("role-sync.resync-delay", 5); + } + + public boolean isRoleSyncPrimaryGroupOnly() { + return config.getBoolean("role-sync.primary-group-only", false); + } + + public Map getRoleSyncGroups() { + return roleSyncGroups; + } + + private Map _getRoleSyncGroups() { + return config.getStringMap("role-sync.groups"); + } + + public Map getRoleSyncRoles() { + return roleSyncRoles; + } + + private Map _getRoleSyncRoles() { + return config.getStringMap("role-sync.roles"); + } + + public enum LinkPolicy { + KICK, + FREEZE, + NONE; + + static LinkPolicy fromName(final String name) { + for (LinkPolicy policy : values()) { + if (policy.name().equalsIgnoreCase(name)) { + return policy; + } + } + return LinkPolicy.NONE; + } + } + + @Override + public void reloadConfig() { + config.load(); + + linkPolicy = LinkPolicy.fromName(config.getString("link-policy", "none")); + roleSyncGroups = _getRoleSyncGroups(); + roleSyncRoles = _getRoleSyncRoles(); + + plugin.onReload(); + } +} diff --git a/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/EssentialsDiscordLink.java b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/EssentialsDiscordLink.java new file mode 100644 index 000000000..f34898e54 --- /dev/null +++ b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/EssentialsDiscordLink.java @@ -0,0 +1,135 @@ +package net.essentialsx.discordlink; + +import com.earth2me.essentials.EssentialsLogger; +import com.earth2me.essentials.IEssentials; +import com.earth2me.essentials.metrics.MetricsWrapper; +import com.google.common.collect.ImmutableSet; +import net.essentialsx.api.v2.services.discord.DiscordService; +import net.essentialsx.api.v2.services.discord.InteractionException; +import net.essentialsx.api.v2.services.discordlink.DiscordLinkService; +import net.essentialsx.discord.EssentialsDiscord; +import net.essentialsx.discordlink.commands.discord.AccountInteractionCommand; +import net.essentialsx.discordlink.commands.discord.LinkInteractionCommand; +import net.essentialsx.discordlink.commands.discord.UnlinkInteractionCommand; +import net.essentialsx.discordlink.listeners.LinkBukkitListener; +import net.essentialsx.discordlink.rolesync.RoleSyncManager; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.plugin.ServicePriority; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.IOException; +import java.util.Collections; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static com.earth2me.essentials.I18n.tl; + +public class EssentialsDiscordLink extends JavaPlugin { + private transient IEssentials ess; + private transient MetricsWrapper metrics = null; + + private DiscordService api; + private DiscordLinkSettings settings; + private AccountStorage accounts; + private AccountLinkManager linkManager; + private RoleSyncManager roleSyncManager; + + @Override + public void onEnable() { + ess = (IEssentials) getServer().getPluginManager().getPlugin("Essentials"); + final EssentialsDiscord essDiscord = (EssentialsDiscord) getServer().getPluginManager().getPlugin("EssentialsDiscord"); + if (ess == null || !ess.isEnabled() || essDiscord == null || !essDiscord.isEnabled()) { + setEnabled(false); + return; + } + if (!getDescription().getVersion().equals(ess.getDescription().getVersion())) { + getLogger().log(Level.WARNING, tl("versionMismatchAll")); + } + + api = getServer().getServicesManager().load(DiscordService.class); + + settings = new DiscordLinkSettings(this); + ess.addReloadListener(settings); + try { + accounts = new AccountStorage(this); + } catch (IOException e) { + getLogger().log(Level.SEVERE, "Unable to create link accounts file", e); + setEnabled(false); + return; + } + + roleSyncManager = new RoleSyncManager(this); + linkManager = new AccountLinkManager(this, accounts, roleSyncManager); + + getServer().getPluginManager().registerEvents(new LinkBukkitListener(this), this); + getServer().getServicesManager().register(DiscordLinkService.class, linkManager, this, ServicePriority.Normal); + + if (!(api.getInteractionController().getCommand("link") instanceof LinkInteractionCommand)) { + try { + api.getInteractionController().registerCommand(new AccountInteractionCommand(linkManager)); + api.getInteractionController().registerCommand(new LinkInteractionCommand(linkManager)); + api.getInteractionController().registerCommand(new UnlinkInteractionCommand(linkManager)); + } catch (InteractionException e) { + e.printStackTrace(); + setEnabled(false); + return; + } + } + + ess.getPermissionsHandler().registerContext("essentials:linked", user -> + Collections.singleton(String.valueOf(linkManager.isLinked(user.getUUID()))), () -> ImmutableSet.of("true", "false")); + + if (metrics == null) { + metrics = new MetricsWrapper(this, 11462, false); + } + } + + @Override + public void onDisable() { + if (accounts != null) { + accounts.shutdown(); + } + } + + public void onReload() { + if (roleSyncManager != null) { + roleSyncManager.onReload(); + } + } + + public IEssentials getEss() { + return ess; + } + + public DiscordService getApi() { + return api; + } + + public DiscordLinkSettings getSettings() { + return settings; + } + + public AccountStorage getAccountStorage() { + return accounts; + } + + public AccountLinkManager getLinkManager() { + return linkManager; + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + return ess.onCommandEssentials(sender, command, label, args, EssentialsDiscordLink.class.getClassLoader(), "net.essentialsx.discordlink.commands.bukkit.Command", "essentials.", linkManager); + } + + @Override + public Logger getLogger() { + try { + return EssentialsLogger.getLoggerProvider(this); + } catch (Throwable ignored) { + // In case Essentials isn't installed/loaded + return super.getLogger(); + } + } +} diff --git a/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/commands/bukkit/Commandlink.java b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/commands/bukkit/Commandlink.java new file mode 100644 index 000000000..36831fefb --- /dev/null +++ b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/commands/bukkit/Commandlink.java @@ -0,0 +1,30 @@ +package net.essentialsx.discordlink.commands.bukkit; + +import com.earth2me.essentials.User; +import com.earth2me.essentials.commands.EssentialsCommand; +import net.essentialsx.discordlink.AccountLinkManager; +import org.bukkit.Server; + +import static com.earth2me.essentials.I18n.tl; + +public class Commandlink extends EssentialsCommand { + public Commandlink() { + super("link"); + } + + @Override + protected void run(Server server, User user, String commandLabel, String[] args) { + final AccountLinkManager manager = (AccountLinkManager) module; + if (manager.isLinked(user.getUUID())) { + user.sendMessage(tl("discordLinkLinkedAlready")); + return; + } + + try { + final String code = manager.createCode(user.getBase().getUniqueId()); + user.sendMessage(tl("discordLinkLinked", "/link " + code)); + } catch (final IllegalArgumentException e) { + user.sendMessage(tl("discordLinkPending", "/link " + e.getMessage())); + } + } +} diff --git a/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/commands/bukkit/Commandunlink.java b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/commands/bukkit/Commandunlink.java new file mode 100644 index 000000000..4e30f92d2 --- /dev/null +++ b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/commands/bukkit/Commandunlink.java @@ -0,0 +1,26 @@ +package net.essentialsx.discordlink.commands.bukkit; + +import com.earth2me.essentials.User; +import com.earth2me.essentials.commands.EssentialsCommand; +import net.essentialsx.api.v2.events.discordlink.DiscordLinkStatusChangeEvent; +import net.essentialsx.discordlink.AccountLinkManager; +import org.bukkit.Server; + +import static com.earth2me.essentials.I18n.tl; + +public class Commandunlink extends EssentialsCommand { + public Commandunlink() { + super("unlink"); + } + + @Override + protected void run(Server server, User user, String commandLabel, String[] args) { + final AccountLinkManager manager = (AccountLinkManager) module; + if (!manager.removeAccount(user, DiscordLinkStatusChangeEvent.Cause.UNSYNC_PLAYER)) { + user.sendMessage(tl("discordLinkNoAccount")); + return; + } + + user.sendMessage(tl("discordLinkUnlinked")); + } +} diff --git a/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/commands/discord/AccountInteractionCommand.java b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/commands/discord/AccountInteractionCommand.java new file mode 100644 index 000000000..7df811759 --- /dev/null +++ b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/commands/discord/AccountInteractionCommand.java @@ -0,0 +1,66 @@ +package net.essentialsx.discordlink.commands.discord; + +import com.google.common.collect.ImmutableList; +import net.ess3.api.IUser; +import net.essentialsx.api.v2.services.discord.InteractionCommand; +import net.essentialsx.api.v2.services.discord.InteractionCommandArgument; +import net.essentialsx.api.v2.services.discord.InteractionCommandArgumentType; +import net.essentialsx.api.v2.services.discord.InteractionEvent; +import net.essentialsx.api.v2.services.discord.InteractionMember; +import net.essentialsx.discordlink.AccountLinkManager; + +import java.util.List; + +import static com.earth2me.essentials.I18n.tl; + +public class AccountInteractionCommand implements InteractionCommand { + private final List arguments; + private final AccountLinkManager accounts; + + public AccountInteractionCommand(AccountLinkManager accounts) { + this.arguments = ImmutableList.of(new InteractionCommandArgument("user", tl("discordCommandAccountArgumentUser"), InteractionCommandArgumentType.USER, false)); + this.accounts = accounts; + } + + @Override + public boolean isDisabled() { + return false; + } + + @Override + public boolean isEphemeral() { + return true; + } + + @Override + public String getName() { + return "account"; + } + + @Override + public String getDescription() { + return tl("discordCommandAccountDescription"); + } + + @Override + public List getArguments() { + return arguments; + } + + @Override + public void onCommand(InteractionEvent event) { + final InteractionMember userArg = event.getUserArgument("user"); + final InteractionMember effectiveUser = userArg == null ? event.getMember() : userArg; + final IUser user = accounts.getUser(effectiveUser.getId()); + if (user == null) { + event.reply(tl(event.getMember().getId().equals(effectiveUser.getId()) ? "discordCommandAccountResponseNotLinked" : "discordCommandAccountResponseNotLinkedOther", effectiveUser.getAsMention())); + return; + } + + if (event.getMember().getId().equals(effectiveUser.getId())) { + event.reply(tl("discordCommandAccountResponseLinked", user.getName())); + return; + } + event.reply(tl("discordCommandAccountResponseLinkedOther", effectiveUser.getAsMention(), user.getName())); + } +} diff --git a/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/commands/discord/LinkInteractionCommand.java b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/commands/discord/LinkInteractionCommand.java new file mode 100644 index 000000000..8b1f4345f --- /dev/null +++ b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/commands/discord/LinkInteractionCommand.java @@ -0,0 +1,67 @@ +package net.essentialsx.discordlink.commands.discord; + +import com.google.common.collect.ImmutableList; +import net.essentialsx.api.v2.events.discordlink.DiscordLinkStatusChangeEvent; +import net.essentialsx.api.v2.services.discord.InteractionCommand; +import net.essentialsx.api.v2.services.discord.InteractionCommandArgument; +import net.essentialsx.api.v2.services.discord.InteractionCommandArgumentType; +import net.essentialsx.api.v2.services.discord.InteractionEvent; +import net.essentialsx.discordlink.AccountLinkManager; + +import java.util.List; +import java.util.UUID; + +import static com.earth2me.essentials.I18n.tl; + +public class LinkInteractionCommand implements InteractionCommand { + private final List arguments; + private final AccountLinkManager accounts; + + public LinkInteractionCommand(final AccountLinkManager accounts) { + this.arguments = ImmutableList.of(new InteractionCommandArgument("code", tl("discordCommandLinkArgumentCode"), InteractionCommandArgumentType.STRING, true)); + this.accounts = accounts; + } + + @Override + public void onCommand(InteractionEvent event) { + if (accounts.isLinked(event.getMember().getId())) { + event.reply(tl("discordCommandLinkHasAccount")); + return; + } + + final UUID uuid = accounts.getPendingUUID(event.getStringArgument("code")); + if (uuid == null) { + event.reply(tl("discordCommandLinkInvalidCode")); + return; + } + + accounts.registerAccount(uuid, event.getMember(), DiscordLinkStatusChangeEvent.Cause.SYNC_PLAYER); + event.reply(tl("discordCommandLinkLinked")); + } + + @Override + public boolean isDisabled() { + return false; + } + + @Override + public boolean isEphemeral() { + return true; + } + + @Override + public String getName() { + return "link"; + } + + @Override + public String getDescription() { + return tl("discordCommandLinkDescription"); + } + + @Override + public List getArguments() { + return arguments; + } +} + diff --git a/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/commands/discord/UnlinkInteractionCommand.java b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/commands/discord/UnlinkInteractionCommand.java new file mode 100644 index 000000000..690e8102f --- /dev/null +++ b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/commands/discord/UnlinkInteractionCommand.java @@ -0,0 +1,53 @@ +package net.essentialsx.discordlink.commands.discord; + +import net.essentialsx.api.v2.events.discordlink.DiscordLinkStatusChangeEvent; +import net.essentialsx.api.v2.services.discord.InteractionCommand; +import net.essentialsx.api.v2.services.discord.InteractionCommandArgument; +import net.essentialsx.api.v2.services.discord.InteractionEvent; +import net.essentialsx.discordlink.AccountLinkManager; + +import java.util.List; + +import static com.earth2me.essentials.I18n.tl; + +public class UnlinkInteractionCommand implements InteractionCommand { + private final AccountLinkManager accounts; + + public UnlinkInteractionCommand(final AccountLinkManager accounts) { + this.accounts = accounts; + } + + @Override + public void onCommand(InteractionEvent event) { + if (!accounts.removeAccount(event.getMember(), DiscordLinkStatusChangeEvent.Cause.UNSYNC_PLAYER)) { + event.reply(tl("discordCommandUnlinkInvalidCode")); + return; + } + event.reply(tl("discordCommandUnlinkUnlinked")); + } + + @Override + public boolean isDisabled() { + return false; + } + + @Override + public boolean isEphemeral() { + return true; + } + + @Override + public String getName() { + return "unlink"; + } + + @Override + public String getDescription() { + return tl("discordCommandUnlinkDescription"); + } + + @Override + public List getArguments() { + return null; + } +} diff --git a/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/listeners/LinkBukkitListener.java b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/listeners/LinkBukkitListener.java new file mode 100644 index 000000000..4ea071ff6 --- /dev/null +++ b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/listeners/LinkBukkitListener.java @@ -0,0 +1,179 @@ +package net.essentialsx.discordlink.listeners; + +import com.earth2me.essentials.utils.FormatUtil; +import net.essentialsx.api.v2.events.AsyncUserDataLoadEvent; +import net.essentialsx.api.v2.events.UserMailEvent; +import net.essentialsx.api.v2.events.discord.DiscordMessageEvent; +import net.essentialsx.api.v2.events.discordlink.DiscordLinkStatusChangeEvent; +import net.essentialsx.api.v2.services.discord.MessageType; +import net.essentialsx.discord.util.MessageUtil; +import net.essentialsx.discordlink.DiscordLinkSettings; +import net.essentialsx.discordlink.EssentialsDiscordLink; +import org.bukkit.Bukkit; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.AsyncPlayerChatEvent; +import org.bukkit.event.player.AsyncPlayerPreLoginEvent; +import org.bukkit.event.player.PlayerCommandPreprocessEvent; +import org.bukkit.event.player.PlayerInteractEvent; + +import static com.earth2me.essentials.I18n.tl; + +public class LinkBukkitListener implements Listener { + private final EssentialsDiscordLink ess; + + public LinkBukkitListener(EssentialsDiscordLink ess) { + this.ess = ess; + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onMail(final UserMailEvent event) { + if (!ess.getSettings().isRelayMail()) { + return; + } + + final String discordId = ess.getLinkManager().getDiscordId(event.getRecipient().getBase().getUniqueId()); + if (discordId == null) { + return; + } + + final String sanitizedName = MessageUtil.sanitizeDiscordMarkdown(event.getMessage().getSenderUsername()); + final String sanitizedMessage = MessageUtil.sanitizeDiscordMarkdown(FormatUtil.stripFormat(event.getMessage().getMessage())); + + ess.getApi().getMemberById(discordId).thenAccept(member -> { + member.sendPrivateMessage(tl("discordMailLine", sanitizedName, sanitizedMessage)); + }); + } + + @EventHandler(priority = EventPriority.HIGH) + public void onConnect(final AsyncPlayerPreLoginEvent event) { + if (ess.getSettings().getLinkPolicy() != DiscordLinkSettings.LinkPolicy.KICK) { + return; + } + + if (!ess.getLinkManager().isLinked(event.getUniqueId())) { + String code; + try { + code = ess.getLinkManager().createCode(event.getUniqueId()); + } catch (IllegalArgumentException e) { + code = e.getMessage(); + } + event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, tl("discordLinkLoginKick", "/link " + code, ess.getApi().getInviteUrl())); + } + } + + @EventHandler(priority = EventPriority.LOW) + public void onInteract(final PlayerInteractEvent event) { + if (ess.getSettings().getLinkPolicy() != DiscordLinkSettings.LinkPolicy.FREEZE) { + return; + } + + if (!ess.getLinkManager().isLinked(event.getPlayer().getUniqueId())) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.LOW) + public void onCommand(final PlayerCommandPreprocessEvent event) { + if (ess.getSettings().getLinkPolicy() != DiscordLinkSettings.LinkPolicy.FREEZE) { + return; + } + + //todo maybe allowed commands + if (!ess.getLinkManager().isLinked(event.getPlayer().getUniqueId())) { + event.setCancelled(true); + String code; + try { + code = ess.getLinkManager().createCode(event.getPlayer().getUniqueId()); + } catch (IllegalArgumentException e) { + code = e.getMessage(); + } + event.getPlayer().sendMessage(tl("discordLinkLoginPrompt", "/link " + code, ess.getApi().getInviteUrl())); + } + } + + @EventHandler(priority = EventPriority.LOW) + public void onChat(final AsyncPlayerChatEvent event) { + if (ess.getSettings().getLinkPolicy() != DiscordLinkSettings.LinkPolicy.FREEZE) { + return; + } + + if (!ess.getLinkManager().isLinked(event.getPlayer().getUniqueId())) { + event.setCancelled(true); + String code; + try { + code = ess.getLinkManager().createCode(event.getPlayer().getUniqueId()); + } catch (IllegalArgumentException e) { + code = e.getMessage(); + } + event.getPlayer().sendMessage(tl("discordLinkLoginPrompt", "/link " + code, ess.getApi().getInviteUrl())); + } + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onUserDataLoad(final AsyncUserDataLoadEvent event) { + if (ess.getSettings().getLinkPolicy() != DiscordLinkSettings.LinkPolicy.FREEZE) { + return; + } + + if (!ess.getLinkManager().isLinked(event.getUser().getBase().getUniqueId())) { + event.getUser().setFreeze(true); + String code; + try { + code = ess.getLinkManager().createCode(event.getUser().getBase().getUniqueId()); + } catch (IllegalArgumentException e) { + code = e.getMessage(); + } + event.getUser().sendMessage(tl("discordLinkLoginPrompt", "/link " + code, ess.getApi().getInviteUrl())); + } + } + + @EventHandler(priority = EventPriority.HIGH) + public void onDiscordMessage(final DiscordMessageEvent event) { + if (ess.getSettings().isBlockUnlinkedChat() && event.getType() == MessageType.DefaultTypes.CHAT && !ess.getLinkManager().isLinked(event.getUUID())) { + event.setCancelled(true); + } + } + + @EventHandler + public void onUserLinkStatusChange(final DiscordLinkStatusChangeEvent event) { + if (event.isLinked()) { + event.getUser().setFreeze(false); + return; + } + + switch (ess.getSettings().getLinkPolicy()) { + case KICK: { + String code; + try { + code = ess.getLinkManager().createCode(event.getUser().getBase().getUniqueId()); + } catch (IllegalArgumentException e) { + code = e.getMessage(); + } + final String finalCode = code; + final Runnable kickTask = () -> event.getUser().getBase().kickPlayer(tl("discordLinkLoginKick", "/link " + finalCode, ess.getApi().getInviteUrl())); + if (Bukkit.isPrimaryThread()) { + kickTask.run(); + } else { + ess.getEss().scheduleSyncDelayedTask(kickTask); + } + break; + } + case FREEZE: { + String code; + try { + code = ess.getLinkManager().createCode(event.getUser().getBase().getUniqueId()); + } catch (IllegalArgumentException e) { + code = e.getMessage(); + } + event.getUser().sendMessage(tl("discordLinkLoginPrompt", "/link " + code, ess.getApi().getInviteUrl())); + event.getUser().setFreeze(true); + break; + } + default: { + break; + } + } + } +} diff --git a/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/rolesync/RoleSyncManager.java b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/rolesync/RoleSyncManager.java new file mode 100644 index 000000000..820678754 --- /dev/null +++ b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/rolesync/RoleSyncManager.java @@ -0,0 +1,192 @@ +package net.essentialsx.discordlink.rolesync; + +import com.earth2me.essentials.UUIDPlayer; +import com.google.common.collect.BiMap; +import net.essentialsx.api.v2.events.discordlink.DiscordLinkStatusChangeEvent; +import net.essentialsx.api.v2.services.discord.InteractionMember; +import net.essentialsx.api.v2.services.discord.InteractionRole; +import net.essentialsx.discordlink.EssentialsDiscordLink; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static com.earth2me.essentials.I18n.tl; + +public class RoleSyncManager implements Listener { + private final EssentialsDiscordLink ess; + private final Map groupToRoleMap = new HashMap<>(); + private final Map roleIdToGroupMap = new HashMap<>(); + + public RoleSyncManager(final EssentialsDiscordLink ess) { + this.ess = ess; + Bukkit.getPluginManager().registerEvents(this, ess); + onReload(); + this.ess.getEss().runTaskTimerAsynchronously(() -> { + if (groupToRoleMap.isEmpty() && roleIdToGroupMap.isEmpty()) { + return; + } + + final BiMap uuidToDiscordCopy = ess.getAccountStorage().getRawStorageMap(); + final Map groupToRoleMapCopy = new HashMap<>(groupToRoleMap); + final Map roleIdToGroupMapCopy = new HashMap<>(roleIdToGroupMap); + final boolean primaryOnly = ess.getSettings().isRoleSyncPrimaryGroupOnly(); + final boolean removeGroups = ess.getSettings().isRoleSyncRemoveGroups(); + final boolean removeRoles = ess.getSettings().isRoleSyncRemoveRoles(); + for (final Map.Entry entry : uuidToDiscordCopy.entrySet()) { + sync(new UUIDPlayer(UUID.fromString(entry.getKey())), entry.getValue(), groupToRoleMapCopy, roleIdToGroupMapCopy, primaryOnly, removeGroups, removeRoles); + } + }, 0, ess.getSettings().getRoleSyncResyncDelay() * 1200L); + } + + public void sync(final UUID uuid, final String discordId) { + final Map groupToRoleMapCopy = new HashMap<>(groupToRoleMap); + final Map roleIdToGroupMapCopy = new HashMap<>(roleIdToGroupMap); + final boolean primaryOnly = ess.getSettings().isRoleSyncPrimaryGroupOnly(); + final boolean removeGroups = ess.getSettings().isRoleSyncRemoveGroups(); + final boolean removeRoles = ess.getSettings().isRoleSyncRemoveRoles(); + sync(new UUIDPlayer(uuid), discordId, groupToRoleMapCopy, roleIdToGroupMapCopy, primaryOnly, removeGroups, removeRoles); + } + + public void sync(final Player player, final String discordId, final Map groupToRoleMap, final Map roleIdToGroupMap, + final boolean primaryOnly, final boolean removeGroups, final boolean removeRoles) { + final List groups = primaryOnly ? + Collections.singletonList(ess.getEss().getPermissionsHandler().getGroup(player)) : ess.getEss().getPermissionsHandler().getGroups(player); + final InteractionMember member = ess.getApi().getMemberById(discordId).join(); + + if (member == null) { + if (ess.getSettings().isUnlinkOnLeave()) { + ess.getLinkManager().removeAccount(ess.getEss().getUser(player.getUniqueId()), DiscordLinkStatusChangeEvent.Cause.UNSYNC_LEAVE); + } else { + unSync(player.getUniqueId(), discordId); + } + return; + } + + final List toAdd = new ArrayList<>(); + final List toRemove = new ArrayList<>(); + + for (final Map.Entry entry : groupToRoleMap.entrySet()) { + if (groups.contains(entry.getKey()) && !member.hasRole(entry.getValue())) { + toAdd.add(entry.getValue()); + } else if (removeRoles && !groups.contains(entry.getKey()) && member.hasRole(entry.getValue())) { + toRemove.add(entry.getValue()); + } + } + + for (final Map.Entry entry : roleIdToGroupMap.entrySet()) { + if (member.hasRole(entry.getKey()) && !groups.contains(entry.getValue())) { + ess.getEss().getPermissionsHandler().addToGroup(player, entry.getValue()); + } else if (removeGroups && !member.hasRole(entry.getKey()) && groups.contains(entry.getValue())) { + ess.getEss().getPermissionsHandler().removeFromGroup(player, entry.getValue()); + } + } + + if (toAdd.isEmpty() && toRemove.isEmpty()) { + return; + } + + ess.getApi().modifyMemberRoles(member, toAdd, toRemove); + } + + public void unSync(final UUID uuid, final String discordId) { + final boolean removeGroups = ess.getSettings().isRoleSyncRemoveGroups(); + final boolean removeRoles = ess.getSettings().isRoleSyncRemoveRoles(); + if (!removeGroups && !removeRoles) { + return; + } + + final Map groupToRoleMapCopy = new HashMap<>(groupToRoleMap); + final Map roleIdToGroupMapCopy = new HashMap<>(roleIdToGroupMap); + + final Player player = new UUIDPlayer(uuid); + final InteractionMember member = ess.getApi().getMemberById(discordId).join(); + + if (removeGroups) { + for (final String group : roleIdToGroupMapCopy.values()) { + ess.getEss().getPermissionsHandler().removeFromGroup(player, group); + } + } + + // Check if the member is no longer in the guild (null), they don't have any roles anyway. + if (removeRoles && member != null) { + ess.getApi().modifyMemberRoles(member, null, groupToRoleMapCopy.values()); + } + } + + @EventHandler + public void onJoin(PlayerJoinEvent event) { + ess.getEss().runTaskAsynchronously(() -> { + if (ess.getLinkManager().isLinked(event.getPlayer().getUniqueId())) { + sync(event.getPlayer().getUniqueId(), ess.getLinkManager().getDiscordId(event.getPlayer().getUniqueId())); + } + }); + } + + public void onReload() { + groupToRoleMap.clear(); + roleIdToGroupMap.clear(); + + final List groups = ess.getEss().getPermissionsHandler().getGroups(); + + for (final Map.Entry entry : ess.getSettings().getRoleSyncGroups().entrySet()) { + if (isExampleRole(entry.getValue())) { + continue; + } + + final String group = entry.getKey(); + final InteractionRole role = ess.getApi().getRole(entry.getValue()); + if (!groups.contains(group)) { + ess.getLogger().warning(tl("discordLinkInvalidGroup", group, entry.getValue(), groups)); + continue; + } + if (role == null) { + ess.getLogger().warning(tl("discordLinkInvalidRole", entry.getValue(), group)); + continue; + } + + if (role.isManaged() || role.isPublicRole()) { + ess.getLogger().warning(tl("discordLinkInvalidRoleManaged", role.getName(), role.getId())); + continue; + } + + if (!role.canInteract()) { + ess.getLogger().warning(tl("discordLinkInvalidRoleInteract", role.getName(), role.getId())); + continue; + } + + groupToRoleMap.put(group, role); + } + + for (final Map.Entry entry : ess.getSettings().getRoleSyncRoles().entrySet()) { + if (isExampleRole(entry.getKey())) { + continue; + } + + final InteractionRole role = ess.getApi().getRole(entry.getKey()); + final String group = entry.getValue(); + if (role == null) { + ess.getLogger().warning(tl("discordLinkInvalidRole", entry.getKey(), group)); + continue; + } + if (!groups.contains(group)) { + ess.getLogger().warning(tl("discordLinkInvalidGroup", group, entry.getKey(), groups)); + continue; + } + + roleIdToGroupMap.put(role.getId(), group); + } + } + + private boolean isExampleRole(final String role) { + return role.equals("0") || role.equals("11111111111111111") || role.equals("22222222222222222") || role.equals("33333333333333333"); + } +} diff --git a/EssentialsDiscordLink/src/main/resources/config.yml b/EssentialsDiscordLink/src/main/resources/config.yml new file mode 100644 index 000000000..f7604bc90 --- /dev/null +++ b/EssentialsDiscordLink/src/main/resources/config.yml @@ -0,0 +1,52 @@ +############################################################# +# +-------------------------------------------------------+ # +# | EssentialsX DiscordLink | # +# +-------------------------------------------------------+ # +############################################################# + +# This is the config file for EssentialsXDiscordLink. +# This config was generated for version ${full.version}. + +# The desired behavior when a player hasn't linked their Minecraft account to Discord. +# Accepts the following values: +# - kick: Kicks the player with a link code and requires they link their discord account before they can join. +# - freeze: Prevents player from moving/interacting/doing commands when they join until they link their discord account. +# - none: Places no restrictions on players for unlinked accounts. +link-policy: none + +# Whether to ignore Discord messages from unlinked members and hide them from Minecraft chat. +block-unlinked-chat: false + +# Whether someone's Minecraft account should be unlinked when they leave the Discord server. +unlink-on-leave: true + +# Whether linked player's incoming mail should be DM'd to them on Discord. +relay-mail: true + +# MC group to Discord role sync settings +# Allows for the ability to give players discord roles based on their Minecraft groups and/or give players Minecraft +# groups based on their Discord roles. +role-sync: + # Whether EssentialsX DiscordLink should remove synced Discord roles from players who unlink their Minecraft account, + # leave the Discord server, or who no longer have the groups that awarded them the role in the first place. + remove-roles: true + # Whether EssentialsX DiscordLink should remove synced Minecraft groups from players who unlink their Discord account, + # or who no longer have the Discord roles that awarded them the group in the first place. + remove-groups: true + # The amount of time (in minutes) between which EssentialsX DiscordLink should audit player groups/roles. + # Requires a restart after changing. + resync-delay: 5 + # Whether EssentialsX DiscordLink should only consider the primary group of Minecraft users + primary-group-only: false + # Minecraft group to Discord role ID synchronization. + # Players in the following groups listed here will receive the corresponding role ID on discord when they link + # their Minecraft account to their Discord account. + groups: + default: 000000000000000000 + admin: 11111111111111111 + # Discord role ID to Minecraft group synchronization. + # Users with the following roles listed here will receive the corresponding group in Minecraft when they link + # their Discord account to their Minecraft account. + roles: + 22222222222222222: vip + 33333333333333333: booster diff --git a/EssentialsDiscordLink/src/main/resources/plugin.yml b/EssentialsDiscordLink/src/main/resources/plugin.yml new file mode 100644 index 000000000..49df5f7f3 --- /dev/null +++ b/EssentialsDiscordLink/src/main/resources/plugin.yml @@ -0,0 +1,18 @@ +name: EssentialsDiscordLink +main: net.essentialsx.discordlink.EssentialsDiscordLink +# Note to developers: This next line cannot change, or the automatic versioning system will break. +version: ${full.version} +website: https://essentialsx.net/ +description: EssentialsX Discord addon which allows you link your Minecraft and Discord accounts together. +authors: [JRoy] +depend: [EssentialsDiscord] +api-version: 1.13 +commands: + link: + description: Generates a code to link your Minecraft account to Discord. + usage: / + aliases: [elink, discordlink, ediscordlink] + unlink: + description: Unlinks your Minecraft account from any associated Discord account. + usage: / + aliases: [eunlink, discordunlink, ediscordunlink] diff --git a/README.md b/README.md index b21e504dc..3ad247fa5 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ however, have some new requirements: * **EssentialsX requires CraftBukkit, Spigot or Paper to run.** Other server software may work, but these are not tested by the team and we may not be able to help with any issues that occur. * **EssentialsX currently supports Minecraft versions 1.8.8, 1.9.4, 1.10.2, 1.11.2, 1.12.2, 1.13.2, 1.14.4, 1.15.2, - 1.16.5, 1.17.1, 1.18.2, and 1.19.2.** + 1.16.5, 1.17.1, 1.18.2, and 1.19.3.** * **EssentialsX currently requires Java 8 or higher.** We recommend using the latest Java version supported by your server software. * **EssentialsX requires [Vault](http://dev.bukkit.org/bukkit-plugins/vault/) to enable using chat prefix/suffixes and diff --git a/build-logic/src/main/kotlin/essentials.base-conventions.gradle.kts b/build-logic/src/main/kotlin/essentials.base-conventions.gradle.kts index 48da7dc4b..09fee8162 100644 --- a/build-logic/src/main/kotlin/essentials.base-conventions.gradle.kts +++ b/build-logic/src/main/kotlin/essentials.base-conventions.gradle.kts @@ -10,7 +10,7 @@ plugins { val baseExtension = extensions.create("essentials", project) val checkstyleVersion = "8.36.2" -val spigotVersion = "1.19.2-R0.1-SNAPSHOT" +val spigotVersion = "1.19.3-R0.1-SNAPSHOT" val junit5Version = "5.7.0" val mockitoVersion = "3.2.0" diff --git a/providers/1_8Provider/src/main/java/com/earth2me/essentials/UUIDPlayer.java b/providers/1_8Provider/src/main/java/com/earth2me/essentials/UUIDPlayer.java new file mode 100644 index 000000000..5994a25f9 --- /dev/null +++ b/providers/1_8Provider/src/main/java/com/earth2me/essentials/UUIDPlayer.java @@ -0,0 +1,1314 @@ +package com.earth2me.essentials; + +import org.bukkit.Achievement; +import org.bukkit.Bukkit; +import org.bukkit.Effect; +import org.bukkit.EntityEffect; +import org.bukkit.GameMode; +import org.bukkit.Instrument; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.Note; +import org.bukkit.Server; +import org.bukkit.Sound; +import org.bukkit.Statistic; +import org.bukkit.WeatherType; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.conversations.Conversation; +import org.bukkit.conversations.ConversationAbandonedEvent; +import org.bukkit.entity.Arrow; +import org.bukkit.entity.Egg; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.bukkit.entity.Projectile; +import org.bukkit.entity.Snowball; +import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.event.player.PlayerTeleportEvent; +import org.bukkit.inventory.EntityEquipment; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryView; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; +import org.bukkit.map.MapView; +import org.bukkit.metadata.MetadataValue; +import org.bukkit.permissions.Permission; +import org.bukkit.permissions.PermissionAttachment; +import org.bukkit.permissions.PermissionAttachmentInfo; +import org.bukkit.plugin.Plugin; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; +import org.bukkit.scoreboard.Scoreboard; +import org.bukkit.util.Vector; + +import java.net.InetSocketAddress; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +public class UUIDPlayer implements Player { + private final UUID uuid; + + public UUIDPlayer(UUID uuid) { + this.uuid = uuid; + } + + @Override + public UUID getUniqueId() { + return uuid; + } + + @Override + public boolean isOnline() { + return false; + } + + @Override + public String getName() { + return uuid.toString(); + } + + @Override + public Server getServer() { + return Bukkit.getServer(); + } + + @Override + public String getDisplayName() { + throw new UnsupportedOperationException(); + } + + @Override + public void setDisplayName(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public String getPlayerListName() { + throw new UnsupportedOperationException(); + } + + @Override + public void setPlayerListName(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public void setCompassTarget(Location loc) { + throw new UnsupportedOperationException(); + } + + @Override + public Location getCompassTarget() { + throw new UnsupportedOperationException(); + } + + @Override + public InetSocketAddress getAddress() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isConversing() { + throw new UnsupportedOperationException(); + } + + @Override + public void acceptConversationInput(String input) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean beginConversation(Conversation conversation) { + throw new UnsupportedOperationException(); + } + + @Override + public void abandonConversation(Conversation conversation) { + throw new UnsupportedOperationException(); + } + + @Override + public void abandonConversation(Conversation conversation, ConversationAbandonedEvent details) { + throw new UnsupportedOperationException(); + } + + @Override + public void sendRawMessage(String message) { + throw new UnsupportedOperationException(); + } + + @Override + public void kickPlayer(String message) { + throw new UnsupportedOperationException(); + } + + @Override + public void chat(String msg) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean performCommand(String command) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isSneaking() { + throw new UnsupportedOperationException(); + } + + @Override + public void setSneaking(boolean sneak) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isSprinting() { + throw new UnsupportedOperationException(); + } + + @Override + public void setSprinting(boolean sprinting) { + throw new UnsupportedOperationException(); + } + + @Override + public void saveData() { + throw new UnsupportedOperationException(); + } + + @Override + public void loadData() { + throw new UnsupportedOperationException(); + } + + @Override + public void setSleepingIgnored(boolean isSleeping) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isSleepingIgnored() { + throw new UnsupportedOperationException(); + } + + @Override + public void playNote(Location loc, byte instrument, byte note) { + throw new UnsupportedOperationException(); + } + + @Override + public void playNote(Location loc, Instrument instrument, Note note) { + throw new UnsupportedOperationException(); + } + + @Override + public void playSound(Location location, Sound sound, float volume, float pitch) { + throw new UnsupportedOperationException(); + } + + @Override + public void playSound(Location location, String sound, float volume, float pitch) { + throw new UnsupportedOperationException(); + } + + @Override + public void playEffect(Location loc, Effect effect, int data) { + throw new UnsupportedOperationException(); + } + + @Override + public void playEffect(Location loc, Effect effect, T data) { + throw new UnsupportedOperationException(); + } + + @Override + public void sendBlockChange(Location loc, Material material, byte data) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean sendChunkChange(Location loc, int sx, int sy, int sz, byte[] data) { + throw new UnsupportedOperationException(); + } + + @Override + public void sendBlockChange(Location loc, int material, byte data) { + throw new UnsupportedOperationException(); + } + + @Override + public void sendSignChange(Location loc, String[] lines) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public void sendMap(MapView map) { + throw new UnsupportedOperationException(); + } + + @Override + public void updateInventory() { + throw new UnsupportedOperationException(); + } + + @Override + public void awardAchievement(Achievement achievement) { + throw new UnsupportedOperationException(); + } + + @Override + public void removeAchievement(Achievement achievement) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasAchievement(Achievement achievement) { + throw new UnsupportedOperationException(); + } + + @Override + public void incrementStatistic(Statistic statistic) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public void decrementStatistic(Statistic statistic) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public void incrementStatistic(Statistic statistic, int amount) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public void decrementStatistic(Statistic statistic, int amount) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public void setStatistic(Statistic statistic, int newValue) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public int getStatistic(Statistic statistic) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public void incrementStatistic(Statistic statistic, Material material) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public void decrementStatistic(Statistic statistic, Material material) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public int getStatistic(Statistic statistic, Material material) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public void incrementStatistic(Statistic statistic, Material material, int amount) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public void decrementStatistic(Statistic statistic, Material material, int amount) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public void setStatistic(Statistic statistic, Material material, int newValue) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public void incrementStatistic(Statistic statistic, EntityType entityType) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public void decrementStatistic(Statistic statistic, EntityType entityType) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public int getStatistic(Statistic statistic, EntityType entityType) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public void incrementStatistic(Statistic statistic, EntityType entityType, int amount) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public void decrementStatistic(Statistic statistic, EntityType entityType, int amount) { + throw new UnsupportedOperationException(); + } + + @Override + public void setStatistic(Statistic statistic, EntityType entityType, int newValue) { + throw new UnsupportedOperationException(); + } + + @Override + public void setPlayerTime(long time, boolean relative) { + throw new UnsupportedOperationException(); + } + + @Override + public long getPlayerTime() { + throw new UnsupportedOperationException(); + } + + @Override + public long getPlayerTimeOffset() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isPlayerTimeRelative() { + throw new UnsupportedOperationException(); + } + + @Override + public void resetPlayerTime() { + throw new UnsupportedOperationException(); + } + + @Override + public void setPlayerWeather(WeatherType type) { + throw new UnsupportedOperationException(); + } + + @Override + public WeatherType getPlayerWeather() { + throw new UnsupportedOperationException(); + } + + @Override + public void resetPlayerWeather() { + throw new UnsupportedOperationException(); + } + + @Override + public void giveExp(int amount) { + throw new UnsupportedOperationException(); + } + + @Override + public void giveExpLevels(int amount) { + throw new UnsupportedOperationException(); + } + + @Override + public float getExp() { + throw new UnsupportedOperationException(); + } + + @Override + public void setExp(float exp) { + throw new UnsupportedOperationException(); + } + + @Override + public int getLevel() { + throw new UnsupportedOperationException(); + } + + @Override + public void setLevel(int level) { + throw new UnsupportedOperationException(); + } + + @Override + public int getTotalExperience() { + throw new UnsupportedOperationException(); + } + + @Override + public void setTotalExperience(int exp) { + throw new UnsupportedOperationException(); + } + + @Override + public float getExhaustion() { + throw new UnsupportedOperationException(); + } + + @Override + public void setExhaustion(float value) { + throw new UnsupportedOperationException(); + } + + @Override + public float getSaturation() { + throw new UnsupportedOperationException(); + } + + @Override + public void setSaturation(float value) { + throw new UnsupportedOperationException(); + } + + @Override + public int getFoodLevel() { + throw new UnsupportedOperationException(); + } + + @Override + public void setFoodLevel(int value) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isBanned() { + throw new UnsupportedOperationException(); + } + + @Override + public void setBanned(boolean banned) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isWhitelisted() { + throw new UnsupportedOperationException(); + } + + @Override + public void setWhitelisted(boolean value) { + throw new UnsupportedOperationException(); + } + + @Override + public Player getPlayer() { + throw new UnsupportedOperationException(); + } + + @Override + public long getFirstPlayed() { + throw new UnsupportedOperationException(); + } + + @Override + public long getLastPlayed() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasPlayedBefore() { + throw new UnsupportedOperationException(); + } + + @Override + public Location getBedSpawnLocation() { + throw new UnsupportedOperationException(); + } + + @Override + public void setBedSpawnLocation(Location location) { + throw new UnsupportedOperationException(); + } + + @Override + public void setBedSpawnLocation(Location location, boolean force) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean getAllowFlight() { + throw new UnsupportedOperationException(); + } + + @Override + public void setAllowFlight(boolean flight) { + throw new UnsupportedOperationException(); + } + + @Override + public void hidePlayer(Player player) { + throw new UnsupportedOperationException(); + } + + @Override + public void showPlayer(Player player) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean canSee(Player player) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isFlying() { + throw new UnsupportedOperationException(); + } + + @Override + public void setFlying(boolean value) { + throw new UnsupportedOperationException(); + } + + @Override + public void setFlySpeed(float value) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public void setWalkSpeed(float value) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public float getFlySpeed() { + throw new UnsupportedOperationException(); + } + + @Override + public float getWalkSpeed() { + throw new UnsupportedOperationException(); + } + + @Override + public void setTexturePack(String url) { + throw new UnsupportedOperationException(); + } + + @Override + public void setResourcePack(String url) { + throw new UnsupportedOperationException(); + } + + @Override + public Scoreboard getScoreboard() { + throw new UnsupportedOperationException(); + } + + @Override + public void setScoreboard(Scoreboard scoreboard) throws IllegalArgumentException, IllegalStateException { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isHealthScaled() { + throw new UnsupportedOperationException(); + } + + @Override + public void setHealthScaled(boolean scale) { + throw new UnsupportedOperationException(); + } + + @Override + public void setHealthScale(double scale) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public double getHealthScale() { + throw new UnsupportedOperationException(); + } + + @Override + public Entity getSpectatorTarget() { + throw new UnsupportedOperationException(); + } + + @Override + public void setSpectatorTarget(Entity entity) { + throw new UnsupportedOperationException(); + } + + @Override + public void sendTitle(String title, String subtitle) { + throw new UnsupportedOperationException(); + } + + @Override + public void resetTitle() { + throw new UnsupportedOperationException(); + } + + @Override + public Spigot spigot() { + throw new UnsupportedOperationException(); + } + + @Override + public Map serialize() { + throw new UnsupportedOperationException(); + } + + @Override + public PlayerInventory getInventory() { + throw new UnsupportedOperationException(); + } + + @Override + public Inventory getEnderChest() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean setWindowProperty(InventoryView.Property prop, int value) { + throw new UnsupportedOperationException(); + } + + @Override + public InventoryView getOpenInventory() { + throw new UnsupportedOperationException(); + } + + @Override + public InventoryView openInventory(Inventory inventory) { + throw new UnsupportedOperationException(); + } + + @Override + public InventoryView openWorkbench(Location location, boolean force) { + throw new UnsupportedOperationException(); + } + + @Override + public InventoryView openEnchanting(Location location, boolean force) { + throw new UnsupportedOperationException(); + } + + @Override + public void openInventory(InventoryView inventory) { + throw new UnsupportedOperationException(); + } + + @Override + public void closeInventory() { + throw new UnsupportedOperationException(); + } + + @Override + public ItemStack getItemInHand() { + throw new UnsupportedOperationException(); + } + + @Override + public void setItemInHand(ItemStack item) { + throw new UnsupportedOperationException(); + } + + @Override + public ItemStack getItemOnCursor() { + throw new UnsupportedOperationException(); + } + + @Override + public void setItemOnCursor(ItemStack item) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isSleeping() { + throw new UnsupportedOperationException(); + } + + @Override + public int getSleepTicks() { + throw new UnsupportedOperationException(); + } + + @Override + public GameMode getGameMode() { + throw new UnsupportedOperationException(); + } + + @Override + public void setGameMode(GameMode mode) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isBlocking() { + throw new UnsupportedOperationException(); + } + + @Override + public int getExpToLevel() { + throw new UnsupportedOperationException(); + } + + @Override + public double getEyeHeight() { + throw new UnsupportedOperationException(); + } + + @Override + public double getEyeHeight(boolean ignorePose) { + throw new UnsupportedOperationException(); + } + + @Override + public Location getEyeLocation() { + throw new UnsupportedOperationException(); + } + + @Override + public List getLineOfSight(HashSet transparent, int maxDistance) { + throw new UnsupportedOperationException(); + } + + @Override + public List getLineOfSight(Set transparent, int maxDistance) { + throw new UnsupportedOperationException(); + } + + @Override + public Block getTargetBlock(HashSet transparent, int maxDistance) { + throw new UnsupportedOperationException(); + } + + @Override + public Block getTargetBlock(Set transparent, int maxDistance) { + throw new UnsupportedOperationException(); + } + + @Override + public List getLastTwoTargetBlocks(HashSet transparent, int maxDistance) { + throw new UnsupportedOperationException(); + } + + @Override + public List getLastTwoTargetBlocks(Set transparent, int maxDistance) { + throw new UnsupportedOperationException(); + } + + @Override + public Egg throwEgg() { + throw new UnsupportedOperationException(); + } + + @Override + public Snowball throwSnowball() { + throw new UnsupportedOperationException(); + } + + @Override + public Arrow shootArrow() { + throw new UnsupportedOperationException(); + } + + @Override + public int getRemainingAir() { + throw new UnsupportedOperationException(); + } + + @Override + public void setRemainingAir(int ticks) { + throw new UnsupportedOperationException(); + } + + @Override + public int getMaximumAir() { + throw new UnsupportedOperationException(); + } + + @Override + public void setMaximumAir(int ticks) { + throw new UnsupportedOperationException(); + } + + @Override + public int getMaximumNoDamageTicks() { + throw new UnsupportedOperationException(); + } + + @Override + public void setMaximumNoDamageTicks(int ticks) { + throw new UnsupportedOperationException(); + } + + @Override + public double getLastDamage() { + throw new UnsupportedOperationException(); + } + + @Override + public int _INVALID_getLastDamage() { + throw new UnsupportedOperationException(); + } + + @Override + public void setLastDamage(double damage) { + throw new UnsupportedOperationException(); + } + + @Override + public void _INVALID_setLastDamage(int damage) { + throw new UnsupportedOperationException(); + } + + @Override + public int getNoDamageTicks() { + throw new UnsupportedOperationException(); + } + + @Override + public void setNoDamageTicks(int ticks) { + throw new UnsupportedOperationException(); + } + + @Override + public Player getKiller() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addPotionEffect(PotionEffect effect) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addPotionEffect(PotionEffect effect, boolean force) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addPotionEffects(Collection effects) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasPotionEffect(PotionEffectType type) { + throw new UnsupportedOperationException(); + } + + @Override + public void removePotionEffect(PotionEffectType type) { + throw new UnsupportedOperationException(); + } + + @Override + public Collection getActivePotionEffects() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasLineOfSight(Entity other) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean getRemoveWhenFarAway() { + throw new UnsupportedOperationException(); + } + + @Override + public void setRemoveWhenFarAway(boolean remove) { + throw new UnsupportedOperationException(); + } + + @Override + public EntityEquipment getEquipment() { + throw new UnsupportedOperationException(); + } + + @Override + public void setCanPickupItems(boolean pickup) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean getCanPickupItems() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isLeashed() { + throw new UnsupportedOperationException(); + } + + @Override + public Entity getLeashHolder() throws IllegalStateException { + throw new UnsupportedOperationException(); + } + + @Override + public boolean setLeashHolder(Entity holder) { + throw new UnsupportedOperationException(); + } + + @Override + public void damage(double amount) { + throw new UnsupportedOperationException(); + } + + @Override + public void _INVALID_damage(int amount) { + throw new UnsupportedOperationException(); + } + + @Override + public void damage(double amount, Entity source) { + throw new UnsupportedOperationException(); + } + + @Override + public void _INVALID_damage(int amount, Entity source) { + throw new UnsupportedOperationException(); + } + + @Override + public double getHealth() { + throw new UnsupportedOperationException(); + } + + @Override + public int _INVALID_getHealth() { + throw new UnsupportedOperationException(); + } + + @Override + public void setHealth(double health) { + throw new UnsupportedOperationException(); + } + + @Override + public void _INVALID_setHealth(int health) { + throw new UnsupportedOperationException(); + } + + @Override + public double getMaxHealth() { + throw new UnsupportedOperationException(); + } + + @Override + public int _INVALID_getMaxHealth() { + throw new UnsupportedOperationException(); + } + + @Override + public void setMaxHealth(double health) { + throw new UnsupportedOperationException(); + } + + @Override + public void _INVALID_setMaxHealth(int health) { + throw new UnsupportedOperationException(); + } + + @Override + public void resetMaxHealth() { + throw new UnsupportedOperationException(); + } + + @Override + public Location getLocation() { + throw new UnsupportedOperationException(); + } + + @Override + public Location getLocation(Location loc) { + throw new UnsupportedOperationException(); + } + + @Override + public void setVelocity(Vector velocity) { + throw new UnsupportedOperationException(); + } + + @Override + public Vector getVelocity() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isOnGround() { + throw new UnsupportedOperationException(); + } + + @Override + public World getWorld() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean teleport(Location location) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean teleport(Location location, PlayerTeleportEvent.TeleportCause cause) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean teleport(Entity destination) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean teleport(Entity destination, PlayerTeleportEvent.TeleportCause cause) { + throw new UnsupportedOperationException(); + } + + @Override + public List getNearbyEntities(double x, double y, double z) { + throw new UnsupportedOperationException(); + } + + @Override + public int getEntityId() { + throw new UnsupportedOperationException(); + } + + @Override + public int getFireTicks() { + throw new UnsupportedOperationException(); + } + + @Override + public int getMaxFireTicks() { + throw new UnsupportedOperationException(); + } + + @Override + public void setFireTicks(int ticks) { + throw new UnsupportedOperationException(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isDead() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isValid() { + throw new UnsupportedOperationException(); + } + + @Override + public void sendMessage(String message) { + throw new UnsupportedOperationException(); + } + + @Override + public void sendMessage(String[] messages) { + throw new UnsupportedOperationException(); + } + + @Override + public Entity getPassenger() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean setPassenger(Entity passenger) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isEmpty() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean eject() { + throw new UnsupportedOperationException(); + } + + @Override + public float getFallDistance() { + throw new UnsupportedOperationException(); + } + + @Override + public void setFallDistance(float distance) { + throw new UnsupportedOperationException(); + } + + @Override + public void setLastDamageCause(EntityDamageEvent event) { + throw new UnsupportedOperationException(); + } + + @Override + public EntityDamageEvent getLastDamageCause() { + throw new UnsupportedOperationException(); + } + + @Override + public int getTicksLived() { + throw new UnsupportedOperationException(); + } + + @Override + public void setTicksLived(int value) { + throw new UnsupportedOperationException(); + } + + @Override + public void playEffect(EntityEffect type) { + throw new UnsupportedOperationException(); + } + + @Override + public EntityType getType() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isInsideVehicle() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean leaveVehicle() { + throw new UnsupportedOperationException(); + } + + @Override + public Entity getVehicle() { + throw new UnsupportedOperationException(); + } + + @Override + public void setCustomNameVisible(boolean flag) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isCustomNameVisible() { + throw new UnsupportedOperationException(); + } + + @Override + public String getCustomName() { + throw new UnsupportedOperationException(); + } + + @Override + public void setCustomName(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public void setMetadata(String metadataKey, MetadataValue newMetadataValue) { + throw new UnsupportedOperationException(); + } + + @Override + public List getMetadata(String metadataKey) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasMetadata(String metadataKey) { + throw new UnsupportedOperationException(); + } + + @Override + public void removeMetadata(String metadataKey, Plugin owningPlugin) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isPermissionSet(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isPermissionSet(Permission perm) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasPermission(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasPermission(Permission perm) { + throw new UnsupportedOperationException(); + } + + @Override + public PermissionAttachment addAttachment(Plugin plugin, String name, boolean value) { + throw new UnsupportedOperationException(); + } + + @Override + public PermissionAttachment addAttachment(Plugin plugin) { + throw new UnsupportedOperationException(); + } + + @Override + public PermissionAttachment addAttachment(Plugin plugin, String name, boolean value, int ticks) { + throw new UnsupportedOperationException(); + } + + @Override + public PermissionAttachment addAttachment(Plugin plugin, int ticks) { + throw new UnsupportedOperationException(); + } + + @Override + public void removeAttachment(PermissionAttachment attachment) { + throw new UnsupportedOperationException(); + } + + @Override + public void recalculatePermissions() { + throw new UnsupportedOperationException(); + } + + @Override + public Set getEffectivePermissions() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isOp() { + throw new UnsupportedOperationException(); + } + + @Override + public void setOp(boolean value) { + throw new UnsupportedOperationException(); + } + + @Override + public void sendPluginMessage(Plugin source, String channel, byte[] message) { + throw new UnsupportedOperationException(); + } + + @Override + public Set getListeningPluginChannels() { + throw new UnsupportedOperationException(); + } + + @Override + public T launchProjectile(Class projectile) { + throw new UnsupportedOperationException(); + } + + @Override + public T launchProjectile(Class projectile, Vector velocity) { + throw new UnsupportedOperationException(); + } +} diff --git a/providers/NMSReflectionProvider/src/main/java/net/ess3/nms/refl/ReflUtil.java b/providers/NMSReflectionProvider/src/main/java/net/ess3/nms/refl/ReflUtil.java index 25362d7cc..61a97f38f 100644 --- a/providers/NMSReflectionProvider/src/main/java/net/ess3/nms/refl/ReflUtil.java +++ b/providers/NMSReflectionProvider/src/main/java/net/ess3/nms/refl/ReflUtil.java @@ -21,6 +21,7 @@ public final class ReflUtil { public static final NMSVersion V1_17_R1 = NMSVersion.fromString("v1_17_R1"); public static final NMSVersion V1_18_R1 = NMSVersion.fromString("v1_18_R1"); public static final NMSVersion V1_19_R1 = NMSVersion.fromString("v1_19_R1"); + public static final NMSVersion V1_19_R2 = NMSVersion.fromString("v1_19_R2"); private static final Map> classCache = new HashMap<>(); private static final Table, String, Method> methodCache = HashBasedTable.create(); private static final Table, MethodParams, Method> methodParamCache = HashBasedTable.create(); diff --git a/providers/NMSReflectionProvider/src/main/java/net/ess3/nms/refl/providers/ReflServerStateProvider.java b/providers/NMSReflectionProvider/src/main/java/net/ess3/nms/refl/providers/ReflServerStateProvider.java index da03e57ad..106473224 100644 --- a/providers/NMSReflectionProvider/src/main/java/net/ess3/nms/refl/providers/ReflServerStateProvider.java +++ b/providers/NMSReflectionProvider/src/main/java/net/ess3/nms/refl/providers/ReflServerStateProvider.java @@ -18,7 +18,9 @@ public class ReflServerStateProvider implements ServerStateProvider { MethodHandle isRunning = null; final String MDFIVEMAGICLETTER; - if (ReflUtil.getNmsVersionObject().isHigherThanOrEqualTo(ReflUtil.V1_19_R1)) { + if (ReflUtil.getNmsVersionObject().isHigherThanOrEqualTo(ReflUtil.V1_19_R2)) { + MDFIVEMAGICLETTER = "v"; + } else if (ReflUtil.getNmsVersionObject().isHigherThanOrEqualTo(ReflUtil.V1_19_R1)) { MDFIVEMAGICLETTER = "u"; } else if (ReflUtil.getNmsVersionObject().isHigherThanOrEqualTo(ReflUtil.V1_18_R1)) { MDFIVEMAGICLETTER = "v"; diff --git a/settings.gradle.kts b/settings.gradle.kts index 8b8c0b786..f133a0cd1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -37,6 +37,7 @@ sequenceOf( "AntiBuild", "Chat", "Discord", + "DiscordLink", "GeoIP", "Protect", "Spawn",