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