diff --git a/src/main/java/eu/mhsl/craftattack/spawn/Main.java b/src/main/java/eu/mhsl/craftattack/spawn/Main.java index 0084c5e..1e26727 100644 --- a/src/main/java/eu/mhsl/craftattack/spawn/Main.java +++ b/src/main/java/eu/mhsl/craftattack/spawn/Main.java @@ -19,6 +19,7 @@ import eu.mhsl.craftattack.spawn.appliances.hotbarRefill.HotbarRefill; import eu.mhsl.craftattack.spawn.appliances.kick.Kick; import eu.mhsl.craftattack.spawn.appliances.knockDoor.KnockDoor; import eu.mhsl.craftattack.spawn.appliances.outlawed.Outlawed; +import eu.mhsl.craftattack.spawn.appliances.packSelect.PackSelect; import eu.mhsl.craftattack.spawn.appliances.panicBan.PanicBan; import eu.mhsl.craftattack.spawn.appliances.playerlimit.PlayerLimit; import eu.mhsl.craftattack.spawn.appliances.portableCrafting.PortableCrafting; @@ -80,8 +81,8 @@ public final class Main extends JavaPlugin { new HotbarRefill(), new ChatMention(), new DoubleDoor(), - new KnockDoor() new KnockDoor(), + new PackSelect(), new GlowingBerries() ); diff --git a/src/main/java/eu/mhsl/craftattack/spawn/appliance/ApplianceCommand.java b/src/main/java/eu/mhsl/craftattack/spawn/appliance/ApplianceCommand.java index 56f7b84..f031610 100644 --- a/src/main/java/eu/mhsl/craftattack/spawn/appliance/ApplianceCommand.java +++ b/src/main/java/eu/mhsl/craftattack/spawn/appliance/ApplianceCommand.java @@ -17,7 +17,7 @@ import java.util.Optional; /** * Utility class which enables command name definition over a constructor. */ -public abstract class ApplianceCommand<T extends Appliance> extends ApplianceSupplier<T> implements TabCompleter, CommandExecutor { +public abstract class ApplianceCommand<T extends Appliance> extends CachedApplianceSupplier<T> implements TabCompleter, CommandExecutor { public String commandName; protected Component errorMessage = Component.text("Fehler: ").color(NamedTextColor.RED); diff --git a/src/main/java/eu/mhsl/craftattack/spawn/appliance/ApplianceListener.java b/src/main/java/eu/mhsl/craftattack/spawn/appliance/ApplianceListener.java index aba9f29..29eb370 100644 --- a/src/main/java/eu/mhsl/craftattack/spawn/appliance/ApplianceListener.java +++ b/src/main/java/eu/mhsl/craftattack/spawn/appliance/ApplianceListener.java @@ -8,6 +8,6 @@ import org.bukkit.event.Listener; * * @param <T> the type of your appliance */ -public abstract class ApplianceListener<T extends Appliance> extends ApplianceSupplier<T> implements Listener { +public abstract class ApplianceListener<T extends Appliance> extends CachedApplianceSupplier<T> implements Listener { } \ No newline at end of file diff --git a/src/main/java/eu/mhsl/craftattack/spawn/appliance/ApplianceSupplier.java b/src/main/java/eu/mhsl/craftattack/spawn/appliance/CachedApplianceSupplier.java similarity index 69% rename from src/main/java/eu/mhsl/craftattack/spawn/appliance/ApplianceSupplier.java rename to src/main/java/eu/mhsl/craftattack/spawn/appliance/CachedApplianceSupplier.java index 28773d3..b4aa4a9 100644 --- a/src/main/java/eu/mhsl/craftattack/spawn/appliance/ApplianceSupplier.java +++ b/src/main/java/eu/mhsl/craftattack/spawn/appliance/CachedApplianceSupplier.java @@ -2,10 +2,10 @@ package eu.mhsl.craftattack.spawn.appliance; import eu.mhsl.craftattack.spawn.Main; -public class ApplianceSupplier<T extends Appliance> implements IApplianceSupplier<T> { +public class CachedApplianceSupplier<T extends Appliance> implements IApplianceSupplier<T> { private final T appliance; - public ApplianceSupplier() { + public CachedApplianceSupplier() { this.appliance = Main.instance().getAppliance(Main.getApplianceType(getClass())); } diff --git a/src/main/java/eu/mhsl/craftattack/spawn/appliances/packSelect/ChangePackCommand.java b/src/main/java/eu/mhsl/craftattack/spawn/appliances/packSelect/ChangePackCommand.java new file mode 100644 index 0000000..12172d0 --- /dev/null +++ b/src/main/java/eu/mhsl/craftattack/spawn/appliances/packSelect/ChangePackCommand.java @@ -0,0 +1,18 @@ +package eu.mhsl.craftattack.spawn.appliances.packSelect; + +import eu.mhsl.craftattack.spawn.appliance.ApplianceCommand; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; + +public class ChangePackCommand extends ApplianceCommand.PlayerChecked<PackSelect> { + public static final String commandName = "texturepack"; + public ChangePackCommand() { + super(commandName); + } + + @Override + protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception { + getAppliance().openPackInventory(getPlayer()); + } +} diff --git a/src/main/java/eu/mhsl/craftattack/spawn/appliances/packSelect/PackConfiguration.java b/src/main/java/eu/mhsl/craftattack/spawn/appliances/packSelect/PackConfiguration.java new file mode 100644 index 0000000..43bbfb2 --- /dev/null +++ b/src/main/java/eu/mhsl/craftattack/spawn/appliances/packSelect/PackConfiguration.java @@ -0,0 +1,94 @@ +package eu.mhsl.craftattack.spawn.appliances.packSelect; + +import com.google.gson.*; +import eu.mhsl.craftattack.spawn.appliance.CachedApplianceSupplier; +import eu.mhsl.craftattack.spawn.util.inventory.HeadBuilder; +import eu.mhsl.craftattack.spawn.util.text.ComponentUtil; +import net.kyori.adventure.resource.ResourcePackInfo; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.*; +import java.util.stream.Collectors; + +public class PackConfiguration extends CachedApplianceSupplier<PackSelect> { + public record Pack(UUID id, String name, String description, String author, ResourcePackInfo info, String icon) { + public ItemStack buildItem() { + ItemStack stack = HeadBuilder.getCustomTextureHead(icon); + ItemMeta meta = stack.getItemMeta(); + meta.itemName(Component.text(id.toString())); + meta.displayName(Component.text(name(), NamedTextColor.GOLD)); + List<Component> lore = new ArrayList<>(); + lore.add(Component.text("Autor: ", NamedTextColor.DARK_GRAY).append(Component.text(author))); + lore.add(Component.text(" ")); + lore.addAll( + ComponentUtil.lineBreak(description()) + .map(s -> Component.text(s, NamedTextColor.GRAY)) + .toList() + ); + lore.add(Component.text(" ")); + meta.lore(lore); + stack.setItemMeta(meta); + return stack; + } + + public boolean equalsItem(ItemStack other) { + String itemName = PlainTextComponentSerializer.plainText().serialize(other.getItemMeta().itemName()); + try { + return UUID.fromString(itemName).equals(id); + } catch(IllegalArgumentException ignored) { + return false; + } + } + } + public record PackList(List<Pack> packs) { + public Optional<Pack> findFromItem(ItemStack itemStack) { + return packs.stream() + .filter(pack -> pack.equalsItem(itemStack)) + .findFirst(); + } + } + public record SerializedPackList(List<UUID> packs) {} + + private final PackList packList; + private final Gson gson = new GsonBuilder().create(); + + private PackConfiguration() { + this.packList = new PackList(new ArrayList<>()); + } + + private PackConfiguration(String data) { + SerializedPackList serializedData = gson.fromJson(data, SerializedPackList.class); + + var availablePackMap = getAppliance().availablePacks.packs().stream() + .collect(Collectors.toMap(PackConfiguration.Pack::id, pack -> pack)); + + this.packList = new PackList( + serializedData.packs().stream() + .map(availablePackMap::get) + .filter(Objects::nonNull) + .collect(Collectors.toList()) + ); + + if (this.packList.packs().isEmpty()) throw new IllegalArgumentException("Serialized data does not contain any valid data!"); + } + + public static PackConfiguration deserialize(String data) { + return new PackConfiguration(data); + } + + public static PackConfiguration empty() { + return new PackConfiguration(); + } + + public String serialize() { + return gson.toJson(new SerializedPackList(this.packList.packs().stream().map(Pack::id).toList())); + } + + public List<Pack> getPackList() { + return packList.packs(); + } +} diff --git a/src/main/java/eu/mhsl/craftattack/spawn/appliances/packSelect/PackConfigurationInventory.java b/src/main/java/eu/mhsl/craftattack/spawn/appliances/packSelect/PackConfigurationInventory.java new file mode 100644 index 0000000..c1647a6 --- /dev/null +++ b/src/main/java/eu/mhsl/craftattack/spawn/appliances/packSelect/PackConfigurationInventory.java @@ -0,0 +1,171 @@ +package eu.mhsl.craftattack.spawn.appliances.packSelect; + +import eu.mhsl.craftattack.spawn.Main; +import eu.mhsl.craftattack.spawn.appliance.CachedApplianceSupplier; +import eu.mhsl.craftattack.spawn.util.IteratorUtil; +import eu.mhsl.craftattack.spawn.util.inventory.ItemBuilder; +import eu.mhsl.craftattack.spawn.util.inventory.PlaceholderItems; +import eu.mhsl.craftattack.spawn.util.world.InteractSounds; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.Nullable; + +import java.util.Collections; +import java.util.List; +import java.util.stream.IntStream; + +public class PackConfigurationInventory extends CachedApplianceSupplier<PackSelect> { + private final PackConfiguration packConfiguration; + private final Player inventoryOwner; + private final Inventory inventory; + + private final ItemStack reset = ItemBuilder.of(Material.BARRIER) + .displayName(Component.text("Zurücksetzen", NamedTextColor.RED)) + .lore("Alle gewählten Texturepacks werden entfernt") + .build(); + + private final ItemStack info = ItemBuilder.of(Material.OAK_HANGING_SIGN) + .displayName(Component.text("Texturepacks", NamedTextColor.GOLD)) + .lore( + "Wähle aus der oberen Liste eine oder mehrere Texturepacks aus. " + + "Ändere die anzuwendende Reihenfolge durch Links/Rechtsklick in der unteren Liste. " + + "Klicke auf Speichern um die Änderungen anzuwenden!" + ) + .build(); + + private final ItemStack save = ItemBuilder.of(Material.GREEN_WOOL) + .displayName(Component.text("Anwenden", NamedTextColor.GREEN)) + .lore("Die Ausgewählten Texturepacks werden in der entsprechenden Reihenfolge angewandt. Das Anwenden kann durch den Download je nach Internetgeschwindigkeit eine Weile dauern.") + .build(); + + private final ItemStack unusedSlot = ItemBuilder.of(Material.LIME_STAINED_GLASS_PANE) + .displayName(Component.text("Freier Slot", NamedTextColor.GREEN)) + .lore("Klicke auf ein Texturepack um es hinzuzufügen.") + .build(); + + + public PackConfigurationInventory(PackConfiguration packConfiguration, Player inventoryOwner) { + this.packConfiguration = packConfiguration; + this.inventoryOwner = inventoryOwner; + this.inventory = Bukkit.createInventory(null, 9 * 6, Component.text("Texturepacks")); + this.draw(); + } + + public Inventory getInventory() { + return inventory; + } + + public void handleClick(@Nullable ItemStack clickedItem, int slot, ClickType clickType) { + if(clickedItem == null) return; + if(clickedItem.equals(reset)) reset(); + if(clickedItem.equals(save)) apply(); + + if(slot >= 9 && slot < 9 * 4) { + getAppliance().availablePacks.findFromItem(clickedItem) + .ifPresent(this::toggle); + } + + if(slot >= 9 * 5) { + getAppliance().availablePacks.findFromItem(clickedItem) + .ifPresent(pack -> { + switch(clickType) { + case RIGHT -> move(pack, true); + case LEFT -> move(pack, false); + case MIDDLE -> toggle(pack); + } + }); + } + } + + private void move(PackConfiguration.Pack pack, boolean moveToRight) { + List<PackConfiguration.Pack> packs = packConfiguration.getPackList(); + int index = packs.indexOf(pack); + + if (index != -1) { + int newIndex = moveToRight ? index + 1 : index - 1; + if (newIndex >= 0 && newIndex < packs.size()) Collections.swap(packs, index, newIndex); + } + + InteractSounds.of(inventoryOwner).click(); + draw(); + } + + private void toggle(PackConfiguration.Pack pack) { + if(packConfiguration.getPackList().contains(pack)) { + packConfiguration.getPackList().remove(pack); + } else { + packConfiguration.getPackList().add(pack); + } + + InteractSounds.of(inventoryOwner).click(); + draw(); + } + + private void reset() { + packConfiguration.getPackList().clear(); + InteractSounds.of(inventoryOwner).delete(); + draw(); + } + + private void apply() { + inventoryOwner.closeInventory(); + InteractSounds.of(inventoryOwner).success(); + Bukkit.getScheduler().runTask(Main.instance(), () -> getAppliance().setPack(inventoryOwner, packConfiguration)); + } + + private void draw() { + inventory.clear(); + + inventory.setItem(0, packConfiguration.getPackList().isEmpty() ? PlaceholderItems.grayStainedGlassPane : reset); + IteratorUtil.times(3, () -> inventory.addItem(PlaceholderItems.grayStainedGlassPane)); + inventory.setItem(4, info); + IteratorUtil.times(3, () -> inventory.addItem(PlaceholderItems.grayStainedGlassPane)); + inventory.setItem(8, save); + + IteratorUtil.iterateListInGlobal( + 9, + getAppliance().availablePacks.packs().stream() + .filter(pack -> !packConfiguration.getPackList().contains(pack)) + .limit(9 * 3) + .map(pack -> { + ItemBuilder stack = ItemBuilder.of(pack.buildItem()); + if(packConfiguration.getPackList().contains(pack)) stack.glint(); + return stack.build(); + }) + .toList(), + inventory::setItem + ); + + IntStream.range(9 * 4, 9 * 5) + .forEach(slot -> inventory.setItem(slot, PlaceholderItems.grayStainedGlassPane)); + + IteratorUtil.iterateListInGlobal( + 9 * 5, + IteratorUtil.expandList( + IntStream.range(0, Math.min(packConfiguration.getPackList().size(), 9)) + .mapToObj(i -> { + PackConfiguration.Pack pack = packConfiguration.getPackList().get(i); + ItemBuilder builder = ItemBuilder.of(pack.buildItem()); + + builder + .displayName(existing -> Component.text(String.format("#%s ", i + 1), NamedTextColor.LIGHT_PURPLE).append(existing)) + .appendLore(Component.text("➡ Rechtsklick um nach Rechts zu verschieben", NamedTextColor.AQUA)) + .appendLore(Component.text("⬅ Linksklick um nach Links zu verschieben", NamedTextColor.AQUA)) + .appendLore(Component.text("\uD83D\uDDD1 Mausradklick um zu entfernen", NamedTextColor.AQUA)); + return builder.build(); + }) + .toList(), + 9, + unusedSlot + ), + inventory::setItem + ); + + } +} diff --git a/src/main/java/eu/mhsl/craftattack/spawn/appliances/packSelect/PackSelect.java b/src/main/java/eu/mhsl/craftattack/spawn/appliances/packSelect/PackSelect.java new file mode 100644 index 0000000..90bbfcc --- /dev/null +++ b/src/main/java/eu/mhsl/craftattack/spawn/appliances/packSelect/PackSelect.java @@ -0,0 +1,136 @@ +package eu.mhsl.craftattack.spawn.appliances.packSelect; + +import eu.mhsl.craftattack.spawn.Main; +import eu.mhsl.craftattack.spawn.appliance.Appliance; +import eu.mhsl.craftattack.spawn.appliance.ApplianceCommand; +import eu.mhsl.craftattack.spawn.appliances.packSelect.listeners.ClosePackInventoryListener; +import eu.mhsl.craftattack.spawn.appliances.packSelect.listeners.ClickPackInventoryListener; +import eu.mhsl.craftattack.spawn.appliances.packSelect.listeners.SetPacksOnJoinListener; +import net.kyori.adventure.resource.ResourcePackInfo; +import net.kyori.adventure.resource.ResourcePackRequest; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.entity.Player; +import org.bukkit.event.Listener; +import org.bukkit.inventory.Inventory; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; +import org.jetbrains.annotations.NotNull; + +import java.net.URI; +import java.util.*; + +public class PackSelect extends Appliance { + private static final NamespacedKey packKey = new NamespacedKey(Main.instance(), PackSelect.class.getName().toLowerCase(Locale.ROOT)); + + public final PackConfiguration.PackList availablePacks = new PackConfiguration.PackList(new ArrayList<>()); + public final Map<Player, PackConfigurationInventory> openInventories = new WeakHashMap<>(); + + public PackSelect() { + super("packselect"); + } + + @Override + public void onEnable() { + List<Map<?, ?>> packs = localConfig().getMapList("packs"); + Bukkit.getScheduler().runTaskAsynchronously( + Main.instance(), + () -> packs.stream() + .flatMap(pack -> pack.entrySet().stream()) + .forEach(pack -> { + @SuppressWarnings("unchecked") Map<String, String> packData = (Map<String, String>) pack.getValue(); + + try { + ResourcePackInfo resourcePackInfo = ResourcePackInfoFactory + .createResourcePackInfo(URI.create(packData.get("url")), packData.get("hash")) + .join(); + + PackConfiguration.Pack packToAdd = new PackConfiguration.Pack( + UUID.nameUUIDFromBytes(pack.getKey().toString().getBytes()), + packData.get("name"), + packData.get("description"), + packData.get("author"), + resourcePackInfo, + packData.get("icon") + ); + availablePacks.packs().add(packToAdd); + } catch (Exception e) { + Main.logger().warning(String.format("Failed to add pack %s: %s", packData.get("name"), e.getMessage())); + } + }) + ); + } + + public PackConfiguration getPackConfigurationForPlayer(Player player) { + PersistentDataContainer persistentDataContainer = player.getPersistentDataContainer(); + try { + String serialized = persistentDataContainer.get(packKey, PersistentDataType.STRING); + Objects.requireNonNull(serialized); + return PackConfiguration.deserialize(serialized); + } catch(IllegalArgumentException | NullPointerException exception) { + return PackConfiguration.empty(); + } + } + + public void openPackInventory(Player player) { + PackConfigurationInventory packInventory = new PackConfigurationInventory(getPackConfigurationForPlayer(player), player); + player.openInventory(packInventory.getInventory()); + openInventories.put(player, packInventory); + } + + public void setPack(Player player, PackConfiguration packConfiguration) { + player.getPersistentDataContainer().set(packKey, PersistentDataType.STRING, packConfiguration.serialize()); + + int packCount = packConfiguration.getPackList().size(); + if(packCount > 0) { + player.sendMessage( + Component.text( + String.format("%s heruntergeladen und hinzugefügt...", packCount > 1 ? "Texturenpakete werden" : "Texturenpaket wird"), + NamedTextColor.DARK_GREEN + ) + ); + } + + player.sendResourcePacks( + ResourcePackRequest.resourcePackRequest() + .packs( + packConfiguration.getPackList().stream() + .map(PackConfiguration.Pack::info) + .toList() + .reversed() + ) + .replace(true) + .required(true) + .prompt( + Component.text() + .append(Component.text("Bestätige um fortzufahren! Du kannst deine Entscheidung jederzeit mit ", NamedTextColor.GRAY)) + .append(Component.text(String.format("/%s ", ChangePackCommand.commandName), NamedTextColor.GOLD)) + .append(Component.text("ändern.", NamedTextColor.GRAY)) + .build() + ) + .build() + ); + } + + public boolean isNotPackInventory(Player player, Inventory inventory) { + PackConfigurationInventory packConfigurationInventory = this.openInventories.get(player); + if(packConfigurationInventory == null) return true; + return packConfigurationInventory.getInventory() != inventory; + } + + @Override + protected @NotNull List<ApplianceCommand<?>> commands() { + return List.of(new ChangePackCommand()); + } + + @Override + protected @NotNull List<Listener> listeners() { + return List.of( + new ClosePackInventoryListener(), + new ClickPackInventoryListener(), + new SetPacksOnJoinListener() + ); + } +} diff --git a/src/main/java/eu/mhsl/craftattack/spawn/appliances/packSelect/ResourcePackInfoFactory.java b/src/main/java/eu/mhsl/craftattack/spawn/appliances/packSelect/ResourcePackInfoFactory.java new file mode 100644 index 0000000..8409155 --- /dev/null +++ b/src/main/java/eu/mhsl/craftattack/spawn/appliances/packSelect/ResourcePackInfoFactory.java @@ -0,0 +1,62 @@ +package eu.mhsl.craftattack.spawn.appliances.packSelect; + +import eu.mhsl.craftattack.spawn.Main; +import net.kyori.adventure.resource.ResourcePackInfo; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URI; +import java.security.MessageDigest; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +public class ResourcePackInfoFactory { + + private static boolean isValidHash(@Nullable String hash) { + return hash != null && hash.length() == 40; + } + + public static @NotNull CompletableFuture<ResourcePackInfo> createResourcePackInfo(@NotNull URI resourcePackUrl, @Nullable String hash) { + if (isValidHash(hash)) { + return CompletableFuture.completedFuture( + ResourcePackInfo.resourcePackInfo(UUID.nameUUIDFromBytes(hash.getBytes()), resourcePackUrl, hash) + ); + } + + return CompletableFuture.supplyAsync(() -> { + try { + Main.logger().info(String.format("Start calculating SHA1 Hash of %s", resourcePackUrl)); + HttpURLConnection connection = (HttpURLConnection) resourcePackUrl.toURL().openConnection(); + connection.setRequestMethod("GET"); + connection.connect(); + + try (InputStream inputStream = connection.getInputStream()) { + MessageDigest sha1Digest = MessageDigest.getInstance("SHA-1"); + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + sha1Digest.update(buffer, 0, bytesRead); + } + String sha1Hex = bytesToHex(sha1Digest.digest()); + + Main.logger().info(String.format("Calculating SHA1 Hash of %s completed: %s", resourcePackUrl, sha1Hex)); + return ResourcePackInfo.resourcePackInfo(UUID.nameUUIDFromBytes(sha1Hex.getBytes()), resourcePackUrl, sha1Hex); + } + } catch (Exception e) { + String error = String.format("Error whilst SHA1 calculation of %s: %s", resourcePackUrl, e.getMessage()); + Main.logger().warning(error); + throw new RuntimeException(error); + } + }); + } + + private static String bytesToHex(byte[] bytes) { + StringBuilder hexString = new StringBuilder(bytes.length * 2); + for (byte b : bytes) { + hexString.append(String.format("%02x", b)); + } + return hexString.toString(); + } +} diff --git a/src/main/java/eu/mhsl/craftattack/spawn/appliances/packSelect/listeners/ClickPackInventoryListener.java b/src/main/java/eu/mhsl/craftattack/spawn/appliances/packSelect/listeners/ClickPackInventoryListener.java new file mode 100644 index 0000000..3156961 --- /dev/null +++ b/src/main/java/eu/mhsl/craftattack/spawn/appliances/packSelect/listeners/ClickPackInventoryListener.java @@ -0,0 +1,22 @@ +package eu.mhsl.craftattack.spawn.appliances.packSelect.listeners; + +import eu.mhsl.craftattack.spawn.appliance.ApplianceListener; +import eu.mhsl.craftattack.spawn.appliances.packSelect.PackSelect; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.inventory.InventoryClickEvent; + +public class ClickPackInventoryListener extends ApplianceListener<PackSelect> { + @EventHandler + public void interact(InventoryClickEvent event) { + if(!(event.getWhoClicked() instanceof Player player)) return; + if(getAppliance().isNotPackInventory(player, event.getInventory())) return; + event.setCancelled(true); + + getAppliance().openInventories.get(player).handleClick( + event.getCurrentItem(), + event.getSlot(), + event.getClick() + ); + } +} diff --git a/src/main/java/eu/mhsl/craftattack/spawn/appliances/packSelect/listeners/ClosePackInventoryListener.java b/src/main/java/eu/mhsl/craftattack/spawn/appliances/packSelect/listeners/ClosePackInventoryListener.java new file mode 100644 index 0000000..b64cb88 --- /dev/null +++ b/src/main/java/eu/mhsl/craftattack/spawn/appliances/packSelect/listeners/ClosePackInventoryListener.java @@ -0,0 +1,16 @@ +package eu.mhsl.craftattack.spawn.appliances.packSelect.listeners; + +import eu.mhsl.craftattack.spawn.appliance.ApplianceListener; +import eu.mhsl.craftattack.spawn.appliances.packSelect.PackSelect; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.inventory.InventoryCloseEvent; + +public class ClosePackInventoryListener extends ApplianceListener<PackSelect> { + @EventHandler + public void onClose(InventoryCloseEvent event) { + if(!(event.getPlayer() instanceof Player player)) return; + if(getAppliance().isNotPackInventory(player, event.getInventory())) return; + getAppliance().openInventories.remove(player); + } +} diff --git a/src/main/java/eu/mhsl/craftattack/spawn/appliances/packSelect/listeners/SetPacksOnJoinListener.java b/src/main/java/eu/mhsl/craftattack/spawn/appliances/packSelect/listeners/SetPacksOnJoinListener.java new file mode 100644 index 0000000..85e5d54 --- /dev/null +++ b/src/main/java/eu/mhsl/craftattack/spawn/appliances/packSelect/listeners/SetPacksOnJoinListener.java @@ -0,0 +1,18 @@ +package eu.mhsl.craftattack.spawn.appliances.packSelect.listeners; + +import eu.mhsl.craftattack.spawn.Main; +import eu.mhsl.craftattack.spawn.appliance.ApplianceListener; +import eu.mhsl.craftattack.spawn.appliances.packSelect.PackSelect; +import org.bukkit.Bukkit; +import org.bukkit.event.EventHandler; +import org.bukkit.event.player.PlayerJoinEvent; + +public class SetPacksOnJoinListener extends ApplianceListener<PackSelect> { + @EventHandler + public void onJoin(PlayerJoinEvent event) { + Bukkit.getScheduler().runTask( + Main.instance(), + () -> getAppliance().setPack(event.getPlayer(), getAppliance().getPackConfigurationForPlayer(event.getPlayer())) + ); + } +} diff --git a/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/SettingCategory.java b/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/SettingCategory.java index 5a7bf41..6cf4503 100644 --- a/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/SettingCategory.java +++ b/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/SettingCategory.java @@ -2,6 +2,6 @@ package eu.mhsl.craftattack.spawn.appliances.settings; public enum SettingCategory { Gameplay, - Chat, + Visuals, Misc, } diff --git a/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/Settings.java b/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/Settings.java index 80d4fa0..0d8ec50 100644 --- a/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/Settings.java +++ b/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/Settings.java @@ -61,7 +61,8 @@ public class Settings extends Appliance { new TechnicalTablistSetting(), new SettingsShortcutSetting(), new DoubleDoorSetting(), - new KnockDoorSetting() + new KnockDoorSetting(), + new PackSelectSetting() ); settings.forEach(setting -> setting.initializeFromPlayer(player)); diff --git a/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/datatypes/ActionSetting.java b/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/datatypes/ActionSetting.java new file mode 100644 index 0000000..59e2071 --- /dev/null +++ b/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/datatypes/ActionSetting.java @@ -0,0 +1,50 @@ +package eu.mhsl.craftattack.spawn.appliances.settings.datatypes; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.persistence.PersistentDataContainer; + + +public abstract class ActionSetting extends Setting<Void> { + public ActionSetting() { + super(null); + } + + protected abstract void onAction(Player player, ClickType clickType); + + @Override + public ItemMeta buildMeta(ItemMeta meta) { + meta.displayName(Component.text(title(), NamedTextColor.WHITE)); + meta.lore(buildDescription(description())); + return meta; + } + + @Override + protected void change(Player player, ClickType clickType) { + onAction(player, clickType); + } + + @Override + protected Void defaultValue() { + return null; + } + + @Override + protected void fromStorage(PersistentDataContainer container) {} + + @Override + protected void toStorage(PersistentDataContainer container, Void value) {} + + @Override + public Class<?> dataType() { + return null; + } + + @Override + public Void state() { + return null; + } +} diff --git a/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/datatypes/BoolSetting.java b/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/datatypes/BoolSetting.java index 75189ac..12ccbb5 100644 --- a/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/datatypes/BoolSetting.java +++ b/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/datatypes/BoolSetting.java @@ -3,6 +3,7 @@ package eu.mhsl.craftattack.spawn.appliances.settings.datatypes; import eu.mhsl.craftattack.spawn.appliances.settings.Settings; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; import org.bukkit.event.inventory.ClickType; import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.persistence.PersistentDataContainer; @@ -47,7 +48,7 @@ public abstract class BoolSetting extends Setting<Boolean> { } @Override - protected void change(ClickType clickType) { + protected void change(Player player, ClickType clickType) { this.state = !this.state; } diff --git a/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/datatypes/MultiBoolSetting.java b/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/datatypes/MultiBoolSetting.java index fb61063..7055030 100644 --- a/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/datatypes/MultiBoolSetting.java +++ b/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/datatypes/MultiBoolSetting.java @@ -4,6 +4,7 @@ import com.google.gson.Gson; import eu.mhsl.craftattack.spawn.appliances.settings.Settings; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.entity.Player; import org.bukkit.event.inventory.ClickType; import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.persistence.PersistentDataContainer; @@ -87,7 +88,7 @@ public abstract class MultiBoolSetting<T> extends Setting<T> { } @Override - protected void change(ClickType clickType) { + protected void change(Player player, ClickType clickType) { var recordComponents = this.state.getClass().getRecordComponents(); int currentIndex = IntStream.range(0, recordComponents.length) diff --git a/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/datatypes/SelectSetting.java b/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/datatypes/SelectSetting.java index 7f56cf6..c00c5b6 100644 --- a/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/datatypes/SelectSetting.java +++ b/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/datatypes/SelectSetting.java @@ -5,6 +5,7 @@ import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.Material; import org.bukkit.NamespacedKey; +import org.bukkit.entity.Player; import org.bukkit.event.inventory.ClickType; import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.persistence.PersistentDataContainer; @@ -57,7 +58,7 @@ public abstract class SelectSetting extends Setting<SelectSetting.Options.Option } @Override - protected void change(ClickType clickType) { + protected void change(Player player, ClickType clickType) { int optionModifier = clickType.equals(ClickType.LEFT) ? 1 : -1; List<Options.Option> options = this.options.options; this.state = IntStream.range(0, options.size()) diff --git a/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/datatypes/Setting.java b/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/datatypes/Setting.java index 8fe768e..0fc5580 100644 --- a/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/datatypes/Setting.java +++ b/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/datatypes/Setting.java @@ -37,7 +37,7 @@ public abstract class Setting<TDataType> { } public void triggerChange(Player p, ClickType clickType) { - this.change(clickType); + this.change(p, clickType); toStorage(p.getPersistentDataContainer(), this.state()); } @@ -57,7 +57,7 @@ public abstract class Setting<TDataType> { protected abstract String description(); protected abstract Material icon(); public abstract ItemMeta buildMeta(ItemMeta meta); - protected abstract void change(ClickType clickType); + protected abstract void change(Player player, ClickType clickType); protected abstract TDataType defaultValue(); protected abstract void fromStorage(PersistentDataContainer container); protected abstract void toStorage(PersistentDataContainer container, TDataType value); diff --git a/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/settings/ChatMentionSetting.java b/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/settings/ChatMentionSetting.java index 62eed06..21a43b4 100644 --- a/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/settings/ChatMentionSetting.java +++ b/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/settings/ChatMentionSetting.java @@ -9,7 +9,7 @@ import org.bukkit.Material; public class ChatMentionSetting extends MultiBoolSetting<ChatMentionSetting.ChatMentionConfig> implements CategorizedSetting { @Override public SettingCategory category() { - return SettingCategory.Chat; + return SettingCategory.Visuals; } public record ChatMentionConfig( diff --git a/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/settings/PackSelectSetting.java b/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/settings/PackSelectSetting.java new file mode 100644 index 0000000..cb11af8 --- /dev/null +++ b/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/settings/PackSelectSetting.java @@ -0,0 +1,37 @@ +package eu.mhsl.craftattack.spawn.appliances.settings.settings; + +import eu.mhsl.craftattack.spawn.Main; +import eu.mhsl.craftattack.spawn.appliances.packSelect.PackSelect; +import eu.mhsl.craftattack.spawn.appliances.settings.CategorizedSetting; +import eu.mhsl.craftattack.spawn.appliances.settings.SettingCategory; +import eu.mhsl.craftattack.spawn.appliances.settings.datatypes.ActionSetting; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.ClickType; + +public class PackSelectSetting extends ActionSetting implements CategorizedSetting { + @Override + protected String title() { + return "Texturepacks"; + } + + @Override + protected String description() { + return "Stelle dein persönliches Texturepack aus einer kuratierten Auswahl zusammen und gestalte deine Spielerfahrung individuell."; + } + + @Override + protected Material icon() { + return Material.PAINTING; + } + + @Override + protected void onAction(Player player, ClickType clickType) { + Main.instance().getAppliance(PackSelect.class).openPackInventory(player); + } + + @Override + public SettingCategory category() { + return SettingCategory.Visuals; + } +} diff --git a/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/settings/ShowJoinAndLeaveMessagesSetting.java b/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/settings/ShowJoinAndLeaveMessagesSetting.java index 00d9698..d37c063 100644 --- a/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/settings/ShowJoinAndLeaveMessagesSetting.java +++ b/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/settings/ShowJoinAndLeaveMessagesSetting.java @@ -33,6 +33,6 @@ public class ShowJoinAndLeaveMessagesSetting extends BoolSetting implements Cate @Override public SettingCategory category() { - return SettingCategory.Chat; + return SettingCategory.Visuals; } } diff --git a/src/main/java/eu/mhsl/craftattack/spawn/util/IteratorUtil.java b/src/main/java/eu/mhsl/craftattack/spawn/util/IteratorUtil.java index 1606072..57b07ad 100644 --- a/src/main/java/eu/mhsl/craftattack/spawn/util/IteratorUtil.java +++ b/src/main/java/eu/mhsl/craftattack/spawn/util/IteratorUtil.java @@ -5,9 +5,14 @@ import org.bukkit.GameRule; import org.bukkit.World; import org.bukkit.entity.Player; +import java.util.List; import java.util.Map; +import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; public class IteratorUtil { public static void worlds(Consumer<World> world) { @@ -25,4 +30,25 @@ public class IteratorUtil { public static void setGameRules(Map<GameRule<Boolean>, Boolean> rules, boolean inverse) { rules.forEach((gameRule, value) -> IteratorUtil.worlds(world -> world.setGameRule(gameRule, value ^ inverse))); } + + public static void times(int times, Runnable callback) { + IntStream.range(0, times).forEach(value -> callback.run()); + } + + public static <T> void iterateListInGlobal(int sectionStart, List<T> list, BiConsumer<Integer, T> callback) { + IntStream.range(sectionStart, sectionStart + list.size()) + .forEach(value -> callback.accept(value, list.get(value - sectionStart))); + } + + public static void iterateLocalInGlobal(int sectionStart, int localLength, BiConsumer<Integer, Integer> callback) { + IntStream.range(sectionStart, sectionStart + localLength) + .forEach(value -> callback.accept(value, value + sectionStart)); + } + + public static <T> List<T> expandList(List<T> list, int targetSize, T defaultValue) { + return Stream.concat( + list.stream(), + Stream.generate(() -> defaultValue).limit(Math.max(0, targetSize - list.size())) + ).collect(Collectors.toList()); + } } diff --git a/src/main/java/eu/mhsl/craftattack/spawn/util/inventory/HeadBuilder.java b/src/main/java/eu/mhsl/craftattack/spawn/util/inventory/HeadBuilder.java new file mode 100644 index 0000000..98def19 --- /dev/null +++ b/src/main/java/eu/mhsl/craftattack/spawn/util/inventory/HeadBuilder.java @@ -0,0 +1,22 @@ +package eu.mhsl.craftattack.spawn.util.inventory; + +import com.destroystokyo.paper.profile.PlayerProfile; +import com.destroystokyo.paper.profile.ProfileProperty; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.SkullMeta; + +import java.util.UUID; + +public class HeadBuilder { + public static ItemStack getCustomTextureHead(String base64) { + ItemStack head = new ItemStack(Material.PLAYER_HEAD); + SkullMeta meta = (SkullMeta) head.getItemMeta(); + PlayerProfile profile = Bukkit.createProfile(UUID.nameUUIDFromBytes(base64.getBytes()), null); + profile.setProperty(new ProfileProperty("textures", base64)); + meta.setPlayerProfile(profile); + head.setItemMeta(meta); + return head; + } +} diff --git a/src/main/java/eu/mhsl/craftattack/spawn/util/inventory/ItemBuilder.java b/src/main/java/eu/mhsl/craftattack/spawn/util/inventory/ItemBuilder.java new file mode 100644 index 0000000..a516c21 --- /dev/null +++ b/src/main/java/eu/mhsl/craftattack/spawn/util/inventory/ItemBuilder.java @@ -0,0 +1,92 @@ +package eu.mhsl.craftattack.spawn.util.inventory; + +import eu.mhsl.craftattack.spawn.util.text.ComponentUtil; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Function; + +public class ItemBuilder { + private final ItemStack itemStack; + + @Contract(value = "_ -> new", pure = true) + public static @NotNull ItemBuilder of(Material material) { + return new ItemBuilder(material); + } + + @Contract(value = "_ -> new", pure = true) + public static @NotNull ItemBuilder of(ItemStack itemStack) { + return new ItemBuilder(itemStack); + } + + private ItemBuilder(Material material) { + this.itemStack = ItemStack.of(material); + } + + private ItemBuilder(ItemStack itemStack) { + this.itemStack = itemStack; + } + + public ItemBuilder displayName(Component displayName) { + return this.withMeta(itemMeta -> itemMeta.displayName(displayName)); + } + + public ItemBuilder displayName(Function<Component, Component> process) { + return this.displayName(process.apply(itemStack.displayName())); + } + + public ItemBuilder lore(String text) { + return this.lore(text, 50, NamedTextColor.GRAY); + } + + public ItemBuilder lore(String text, NamedTextColor color) { + return this.lore(text, 50, color); + } + + public ItemBuilder lore(String text, int linebreak, NamedTextColor color) { + return this.withMeta(itemMeta -> itemMeta.lore( + ComponentUtil.lineBreak(text, linebreak) + .map(s -> Component.text(s, color)) + .toList() + )); + } + + public ItemBuilder appendLore(Component text) { + List<Component> lore = itemStack.lore(); + Objects.requireNonNull(lore, "Cannot append lore to Item without lore"); + lore.add(text); + return this.withMeta(itemMeta -> itemMeta.lore(lore)); + } + + public ItemBuilder noStacking() { + return this.withMeta(itemMeta -> itemMeta.setMaxStackSize(1)); + } + + public ItemBuilder glint() { + return this.withMeta(itemMeta -> itemMeta.setEnchantmentGlintOverride(true)); + } + + public ItemBuilder amount(int amount) { + this.itemStack.setAmount(amount); + return this; + } + + public ItemBuilder withMeta(Consumer<ItemMeta> callback) { + ItemMeta meta = this.itemStack.getItemMeta(); + callback.accept(meta); + this.itemStack.setItemMeta(meta); + return this; + } + + public ItemStack build() { + return this.itemStack; + } +} diff --git a/src/main/java/eu/mhsl/craftattack/spawn/util/inventory/PlaceholderItems.java b/src/main/java/eu/mhsl/craftattack/spawn/util/inventory/PlaceholderItems.java new file mode 100644 index 0000000..65080f5 --- /dev/null +++ b/src/main/java/eu/mhsl/craftattack/spawn/util/inventory/PlaceholderItems.java @@ -0,0 +1,13 @@ +package eu.mhsl.craftattack.spawn.util.inventory; + +import net.kyori.adventure.text.Component; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; + +public class PlaceholderItems { + private static final Component emptyName = Component.text(" "); + public static final ItemStack grayStainedGlassPane = ItemBuilder.of(Material.GRAY_STAINED_GLASS_PANE) + .displayName(emptyName) + .noStacking() + .build(); +} diff --git a/src/main/java/eu/mhsl/craftattack/spawn/util/server/Floodgate.java b/src/main/java/eu/mhsl/craftattack/spawn/util/server/Floodgate.java index 428bf44..815137c 100644 --- a/src/main/java/eu/mhsl/craftattack/spawn/util/server/Floodgate.java +++ b/src/main/java/eu/mhsl/craftattack/spawn/util/server/Floodgate.java @@ -1,6 +1,7 @@ package eu.mhsl.craftattack.spawn.util.server; import org.bukkit.entity.Player; +import org.geysermc.cumulus.form.SimpleForm; import org.geysermc.floodgate.api.FloodgateApi; import org.geysermc.floodgate.api.player.FloodgatePlayer; @@ -24,4 +25,22 @@ public class Floodgate { public static void runJavaOnly(Player p, Consumer<Player> callback) { if(!isBedrock(p)) callback.accept(p); } + + public static void throwWithMessageWhenBedrock(Player player) { + if(isBedrock(player)) { + SimpleForm.builder() + .title("Nicht unterstützt") + .content("Bedrock-Spieler werden derzeit für diese Aktion unterstützt! Tut uns Leid.") + .button("Ok") + .build(); + + throw new BedrockNotSupportedException(player); + } + } + + public static class BedrockNotSupportedException extends RuntimeException { + public BedrockNotSupportedException(Player player) { + super(String.format("Bedrock player '%s' tried using an Operation which is unsupported.", player.getName())); + } + } } diff --git a/src/main/java/eu/mhsl/craftattack/spawn/util/text/ComponentUtil.java b/src/main/java/eu/mhsl/craftattack/spawn/util/text/ComponentUtil.java index cdf6821..c7dc29c 100644 --- a/src/main/java/eu/mhsl/craftattack/spawn/util/text/ComponentUtil.java +++ b/src/main/java/eu/mhsl/craftattack/spawn/util/text/ComponentUtil.java @@ -22,6 +22,9 @@ public class ComponentUtil { return Component.text().append(a.appendNewline().append(b)).build(); } + public static Stream<String> lineBreak(String text) { + return lineBreak(text, 50); + } public static Stream<String> lineBreak(String text, int charactersPerLine) { List<String> lines = new ArrayList<>(); String[] words = text.split(" "); diff --git a/src/main/java/eu/mhsl/craftattack/spawn/util/world/InteractSounds.java b/src/main/java/eu/mhsl/craftattack/spawn/util/world/InteractSounds.java new file mode 100644 index 0000000..f03854b --- /dev/null +++ b/src/main/java/eu/mhsl/craftattack/spawn/util/world/InteractSounds.java @@ -0,0 +1,37 @@ +package eu.mhsl.craftattack.spawn.util.world; + +import net.kyori.adventure.key.Key; +import net.kyori.adventure.sound.Sound; +import org.bukkit.entity.Player; + +public class InteractSounds { + private final Player player; + + public static InteractSounds of(Player player) { + return new InteractSounds(player); + } + + private InteractSounds(Player player) { + this.player = player; + } + + private void playSound(org.bukkit.Sound sound) { + player.playSound(getSound(sound.key()), Sound.Emitter.self()); + } + + private Sound getSound(Key soundKey) { + return Sound.sound(soundKey, Sound.Source.PLAYER, 1f, 1f); + } + + public void click() { + playSound(org.bukkit.Sound.UI_BUTTON_CLICK); + } + + public void success() { + playSound(org.bukkit.Sound.ENTITY_PLAYER_LEVELUP); + } + + public void delete() { + playSound(org.bukkit.Sound.ENTITY_SILVERFISH_DEATH); + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index a685b93..f0217b4 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -52,3 +52,13 @@ tablist: outlawed: voluntarily: [] + +packselect: + packs: + - somepack: + name: "Texture pack name" + description: "Texture pack description" + author: "Pack Author(s)" + url: "https://example.com/download/pack.zip" + hash: "" # SHA1 hash of ZIP file (will be auto determined by the server on startup when not set) + icon: "" # base64 player-head texture, can be obtained from sites like https://minecraft-heads.com/ under developers > Value \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index a9e385b..bbbcec3 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -36,4 +36,5 @@ commands: kick: panicBan: vogelfrei: - settings: \ No newline at end of file + settings: + texturepack: \ No newline at end of file