added texture pack selector
This commit is contained in:
parent
f49cca7f33
commit
f89a935c05
@ -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()
|
||||
);
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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 {
|
||||
|
||||
}
|
@ -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()));
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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
|
||||
);
|
||||
|
||||
}
|
||||
}
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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()))
|
||||
);
|
||||
}
|
||||
}
|
@ -2,6 +2,6 @@ package eu.mhsl.craftattack.spawn.appliances.settings;
|
||||
|
||||
public enum SettingCategory {
|
||||
Gameplay,
|
||||
Chat,
|
||||
Visuals,
|
||||
Misc,
|
||||
}
|
||||
|
@ -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));
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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())
|
||||
|
@ -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);
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -33,6 +33,6 @@ public class ShowJoinAndLeaveMessagesSetting extends BoolSetting implements Cate
|
||||
|
||||
@Override
|
||||
public SettingCategory category() {
|
||||
return SettingCategory.Chat;
|
||||
return SettingCategory.Visuals;
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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(" ");
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
|
@ -36,4 +36,5 @@ commands:
|
||||
kick:
|
||||
panicBan:
|
||||
vogelfrei:
|
||||
settings:
|
||||
settings:
|
||||
texturepack:
|
Loading…
x
Reference in New Issue
Block a user