added texture pack selector

This commit is contained in:
Elias Müller 2024-09-17 22:11:59 +02:00
parent f49cca7f33
commit f89a935c05
31 changed files with 867 additions and 15 deletions

View File

@ -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()
);

View File

@ -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);

View File

@ -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 {
}

View File

@ -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()));
}

View File

@ -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());
}
}

View File

@ -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();
}
}

View File

@ -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
);
}
}

View File

@ -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()
);
}
}

View File

@ -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();
}
}

View File

@ -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()
);
}
}

View File

@ -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);
}
}

View File

@ -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()))
);
}
}

View File

@ -2,6 +2,6 @@ package eu.mhsl.craftattack.spawn.appliances.settings;
public enum SettingCategory {
Gameplay,
Chat,
Visuals,
Misc,
}

View File

@ -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));

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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)

View File

@ -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())

View File

@ -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);

View File

@ -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(

View File

@ -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;
}
}

View File

@ -33,6 +33,6 @@ public class ShowJoinAndLeaveMessagesSetting extends BoolSetting implements Cate
@Override
public SettingCategory category() {
return SettingCategory.Chat;
return SettingCategory.Visuals;
}
}

View File

@ -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());
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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();
}

View File

@ -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()));
}
}
}

View File

@ -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(" ");

View File

@ -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);
}
}

View File

@ -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

View File

@ -36,4 +36,5 @@ commands:
kick:
panicBan:
vogelfrei:
settings:
settings:
texturepack: