splittet project to craftattack and varo flavors

This commit is contained in:
2025-04-04 21:35:07 +02:00
parent 6d0913fa0c
commit 4592d53d22
89 changed files with 177 additions and 188 deletions

28
craftattack/build.gradle Normal file
View File

@ -0,0 +1,28 @@
plugins {
id 'java'
id("com.gradleup.shadow") version "8.3.5"
}
dependencies {
implementation project(':core')
implementation project(':common')
compileOnly 'io.papermc.paper:paper-api:1.21.1-R0.1-SNAPSHOT'
compileOnly 'org.geysermc.floodgate:api:2.2.2-SNAPSHOT'
implementation 'org.apache.httpcomponents:httpclient:4.5.14'
implementation 'com.sparkjava:spark-core:2.9.4'
}
configurations {
shadowImplementation.extendsFrom implementation
}
shadowJar {
configurations = [project.configurations.shadowImplementation]
archiveClassifier.set('')
relocate 'org.apache.httpcomponents', 'eu.mhsl.lib.shadow.httpclient'
relocate 'com.sparkjava', 'eu.mhsl.lib.shadow.spark-core'
mergeServiceFiles()
}

View File

@ -0,0 +1,47 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.antiSignEdit;
import eu.mhsl.craftattack.core.appliance.Appliance;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.datatypes.SelectSetting;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.block.sign.SignSide;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public class AntiSignEdit extends Appliance {
@Override
public void onEnable() {
Settings.instance().declareSetting(SignEditSetting.class);
}
public boolean preventSignEdit(Player p, SignSide sign) {
SelectSetting.Options.Option setting = Settings.instance().getSetting(p, Settings.Key.SignEdit, SelectSetting.Options.Option.class);
if(setting.is(SignEditSetting.editable)) return false;
if(setting.is(SignEditSetting.readOnly)) {
p.sendActionBar(Component.text("Das Bearbeiten von Schildern ist in deinen Einstellungen deaktiviert.", NamedTextColor.RED));
return true;
}
if(setting.is(SignEditSetting.editableWhenEmpty)) {
boolean hasText = sign.lines().stream()
.anyMatch(line -> !PlainTextComponentSerializer.plainText().serialize(line).isBlank());
if(hasText) {
p.sendActionBar(Component.text("Das Bearbeiten von Schildern, welch bereits beschrieben sind, ist bei dir deaktiviert.", NamedTextColor.RED));
return true;
}
}
return false;
}
@Override
protected @NotNull List<Listener> listeners() {
return List.of(new OnSignEditListener());
}
}

View File

@ -0,0 +1,15 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.antiSignEdit;
import eu.mhsl.craftattack.core.appliance.ApplianceListener;
import io.papermc.paper.event.player.PlayerOpenSignEvent;
import org.bukkit.block.sign.SignSide;
import org.bukkit.event.EventHandler;
class OnSignEditListener extends ApplianceListener<AntiSignEdit> {
@EventHandler
public void onEdit(PlayerOpenSignEvent event) {
if(event.getCause().equals(PlayerOpenSignEvent.Cause.PLACE)) return;
SignSide signSide = event.getSign().getSide(event.getSide());
event.setCancelled(this.getAppliance().preventSignEdit(event.getPlayer(), signSide));
}
}

View File

@ -0,0 +1,50 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.antiSignEdit;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.CategorizedSetting;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.SettingCategory;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.datatypes.SelectSetting;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import java.util.List;
import java.util.Locale;
public class SignEditSetting extends SelectSetting implements CategorizedSetting {
private static final String namespace = SignEditSetting.class.getSimpleName().toLowerCase(Locale.ROOT);
public static Options.Option editable = new Options.Option("Bearbeitbar", new NamespacedKey(namespace, "editable"));
public static Options.Option editableWhenEmpty = new Options.Option("Bearbeitbar wenn leer", new NamespacedKey(namespace, "emptyeditable"));
public static Options.Option readOnly = new Options.Option("Nicht bearbeitbar", new NamespacedKey(namespace, "readonly"));
public SignEditSetting() {
super(
Settings.Key.SignEdit,
new Options(List.of(editable, editableWhenEmpty, readOnly))
);
}
@Override
protected String title() {
return "Bearbeiten von Schildern";
}
@Override
protected String description() {
return "Bei einem Klick auf ein Schild, kann dieses Bearbeitet werden";
}
@Override
protected Material icon() {
return Material.OAK_SIGN;
}
@Override
protected Options.Option defaultValue() {
return editableWhenEmpty;
}
@Override
public SettingCategory category() {
return SettingCategory.Gameplay;
}
}

View File

@ -0,0 +1,53 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.autoShulker;
import eu.mhsl.craftattack.core.appliance.Appliance;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Material;
import org.bukkit.block.ShulkerBox;
import org.bukkit.entity.Item;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BlockStateMeta;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.List;
public class AutoShulker extends Appliance {
@Override
public void onEnable() {
Settings.instance().declareSetting(AutoShulkerSetting.class);
}
public boolean tryAutoShulker(Player p, Item item) {
ItemStack itemStack = item.getItemStack();
ItemStack offhandStack = p.getInventory().getItemInOffHand();
if(itemStack.getType().equals(Material.SHULKER_BOX)) return false;
if(!offhandStack.getType().equals(Material.SHULKER_BOX)) return false;
BlockStateMeta blockStateMeta = (BlockStateMeta) offhandStack.getItemMeta();
ShulkerBox shulkerBox = (ShulkerBox) blockStateMeta.getBlockState();
HashMap<Integer, ItemStack> leftOver = shulkerBox.getInventory().addItem(itemStack);
if(leftOver.size() > 1)
throw new IllegalStateException("Multiple ItemStacks cannot be processed by AutoShulker!");
if(itemStack.equals(leftOver.get(0))) {
p.sendActionBar(Component.text("Die Shulkerbox ist voll!", NamedTextColor.RED));
return false;
}
if(leftOver.isEmpty()) {
item.remove();
}
blockStateMeta.setBlockState(shulkerBox);
offhandStack.setItemMeta(blockStateMeta);
return true;
}
@Override
protected @NotNull List<Listener> listeners() {
return List.of(new ItemPickupListener());
}
}

View File

@ -0,0 +1,50 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.autoShulker;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.CategorizedSetting;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.SettingCategory;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.datatypes.SelectSetting;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import java.util.List;
import java.util.Locale;
public class AutoShulkerSetting extends SelectSetting implements CategorizedSetting {
private static final String namespace = AutoShulkerSetting.class.getSimpleName().toLowerCase(Locale.ROOT);
public static Options.Option disabled = new Options.Option("Deaktiviert", new NamespacedKey(namespace, "disabled"));
public static Options.Option notFromPlayers = new Options.Option("Keine manuell gedroppten Items", new NamespacedKey(namespace, "noplayerdrops"));
public static Options.Option enabled = new Options.Option("Alle", new NamespacedKey(namespace, "enabled"));
public AutoShulkerSetting() {
super(
Settings.Key.AutoShulker,
new Options(List.of(disabled, notFromPlayers, enabled))
);
}
@Override
protected String title() {
return "Shulker in offhand automatisch befüllen";
}
@Override
protected String description() {
return "Wenn eine Shulker in der zweiten Hand gehalten wird, werden alle aufgesammelten Items in dieser abgelegt";
}
@Override
protected Material icon() {
return Material.SHULKER_BOX;
}
@Override
protected Options.Option defaultValue() {
return disabled;
}
@Override
public SettingCategory category() {
return SettingCategory.Gameplay;
}
}

View File

@ -0,0 +1,20 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.autoShulker;
import eu.mhsl.craftattack.core.appliance.ApplianceListener;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.datatypes.SelectSetting;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.entity.EntityPickupItemEvent;
class ItemPickupListener extends ApplianceListener<AutoShulker> {
@EventHandler
public void onPickup(EntityPickupItemEvent event) {
if(event.getEntity() instanceof Player p) {
SelectSetting.Options.Option setting = Settings.instance().getSetting(p, Settings.Key.AutoShulker, SelectSetting.Options.Option.class);
if(setting.is(AutoShulkerSetting.disabled)) return;
if(setting.is(AutoShulkerSetting.notFromPlayers) && event.getItem().getThrower() != null) return;
event.setCancelled(this.getAppliance().tryAutoShulker(p, event.getItem()));
}
}
}

View File

@ -0,0 +1,11 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.customAdvancements;
public class Advancements {
public static String searchTrouble = "search_trouble";
public static String fleischerchest = "fleischerchest";
public static String craftPixelblock = "craft_pixelblock";
public static String usePixelblock = "use_pixelblock";
public static String start = "start";
public static String winner = "winner";
public static String participateEvent = "participate_event";
}

View File

@ -0,0 +1,12 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.customAdvancements;
import eu.mhsl.craftattack.core.appliance.ApplianceListener;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerJoinEvent;
class ApplyPendingAdvancementsListener extends ApplianceListener<CustomAdvancements> {
@EventHandler
public void onJoin(PlayerJoinEvent event) {
this.getAppliance().applyPendingAdvancements(event.getPlayer());
}
}

View File

@ -0,0 +1,66 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.customAdvancements;
import eu.mhsl.craftattack.core.Main;
import eu.mhsl.craftattack.core.appliance.Appliance;
import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey;
import org.bukkit.advancement.Advancement;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
public class CustomAdvancements extends Appliance {
record PendingAdvancement(UUID receiver, String advancement) {
}
private final List<PendingAdvancement> pendingAdvancements = new ArrayList<>();
public void grantAdvancement(String advancementName, UUID playerUUID) {
Player player = Bukkit.getPlayer(playerUUID);
if(player == null) {
this.addPendingAdvancement(playerUUID, advancementName);
return;
}
try {
NamespacedKey namespacedKey = Objects.requireNonNull(
NamespacedKey.fromString("craftattack_advancements:craftattack/" + advancementName),
String.format("NamespacedKey with '%s' is invalid!", advancementName)
);
Advancement advancement = Objects.requireNonNull(
Bukkit.getAdvancement(namespacedKey),
String.format("The advancement '%s' does not exist!", namespacedKey.asString())
);
player.getAdvancementProgress(advancement).awardCriteria("criteria");
} catch(Exception e) {
Main.logger().info("Advancement " + advancementName + " not found! (is Custom Advancements data pack loaded?)");
throw e;
}
}
public void applyPendingAdvancements(Player player) {
if(this.pendingAdvancements.isEmpty()) return;
List<PendingAdvancement> grantedAdvancements = this.pendingAdvancements.stream()
.filter(pendingAdvancement -> pendingAdvancement.receiver.equals(player.getUniqueId())).toList();
this.pendingAdvancements.removeAll(grantedAdvancements);
grantedAdvancements.forEach(pendingAdvancement -> this.grantAdvancement(pendingAdvancement.advancement(), player.getUniqueId()));
}
private void addPendingAdvancement(UUID receiver, String advancement) {
this.pendingAdvancements.add(new PendingAdvancement(receiver, advancement));
}
@Override
@NotNull
protected List<Listener> listeners() {
return List.of(
new CustomAdvancementsListener(),
new ApplyPendingAdvancementsListener()
);
}
}

View File

@ -0,0 +1,46 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.customAdvancements;
import eu.mhsl.craftattack.core.appliance.ApplianceListener;
import net.kyori.adventure.text.Component;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.inventory.CraftItemEvent;
import org.bukkit.event.player.PlayerChangedWorldEvent;
import org.bukkit.inventory.ItemStack;
class CustomAdvancementsListener extends ApplianceListener<CustomAdvancements> {
@EventHandler
public void onEntityDamageEntity(EntityDamageByEntityEvent event) {
if(!(event.getEntity() instanceof Player damaged)) return;
if(!(event.getDamager() instanceof Player damager)) return;
if(!damager.getInventory().getItemInMainHand().getType().equals(Material.AIR)) return;
if(!damaged.hasPermission("admin")) return;
this.getAppliance().grantAdvancement(Advancements.searchTrouble, damager.getUniqueId());
}
@EventHandler
public void onCraftItem(CraftItemEvent event) {
ItemStack result = event.getInventory().getResult();
if(result == null) return;
if(!(event.getView().getPlayer() instanceof Player player)) return;
if(result.getType() == Material.RED_SHULKER_BOX) {
this.getAppliance().grantAdvancement(Advancements.fleischerchest, player.getUniqueId());
return;
}
if(result.getItemMeta().itemName().equals(Component.text("98fdf0ae-c3ab-4ef7-ae25-efd518d600de"))) {
this.getAppliance().grantAdvancement(Advancements.craftPixelblock, player.getUniqueId());
}
}
@EventHandler
public void onChangeWorld(PlayerChangedWorldEvent event) {
if(!event.getPlayer().getWorld().getName().startsWith("plugins/PixelBlocks/worlds")) return;
this.getAppliance().grantAdvancement(Advancements.usePixelblock, event.getPlayer().getUniqueId());
}
}

View File

@ -0,0 +1,51 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.doubleDoor;
import eu.mhsl.craftattack.core.appliance.Appliance;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.type.Door;
import org.bukkit.event.Listener;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public class DoubleDoor extends Appliance {
@Override
public void onEnable() {
Settings.instance().declareSetting(DoubleDoorSetting.class);
}
public void openNextDoor(Block block) {
BlockData clickedData = block.getBlockData();
if(!(clickedData instanceof Door clickedDoor)) return;
BlockFace neighborFace = this.getNeighborFace(clickedDoor.getFacing(), clickedDoor.getHinge());
Block neighbourBlock = block.getRelative(neighborFace);
BlockData neighbourData = neighbourBlock.getBlockData();
if(!(neighbourData instanceof Door neighbourDoor)) return;
if(!(neighbourDoor.getFacing() == clickedDoor.getFacing())) return;
if(neighbourDoor.getHinge() == clickedDoor.getHinge()) return;
neighbourDoor.setOpen(!clickedDoor.isOpen());
neighbourBlock.setBlockData(neighbourDoor);
}
private @NotNull BlockFace getNeighborFace(BlockFace face, Door.Hinge hinge) {
return switch(face) {
case EAST -> (hinge == Door.Hinge.RIGHT) ? BlockFace.NORTH : BlockFace.SOUTH;
case WEST -> (hinge == Door.Hinge.RIGHT) ? BlockFace.SOUTH : BlockFace.NORTH;
case SOUTH -> (hinge == Door.Hinge.RIGHT) ? BlockFace.EAST : BlockFace.WEST;
case NORTH -> (hinge == Door.Hinge.RIGHT) ? BlockFace.WEST : BlockFace.EAST;
default ->
throw new IllegalStateException(String.format("BlockFace '%s' of clicked door is not valid!", face));
};
}
@Override
protected @NotNull List<Listener> listeners() {
return List.of(new OnDoorInteractListener());
}
}

View File

@ -0,0 +1,38 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.doubleDoor;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.CategorizedSetting;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.SettingCategory;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.datatypes.BoolSetting;
import org.bukkit.Material;
public class DoubleDoorSetting extends BoolSetting implements CategorizedSetting {
public DoubleDoorSetting() {
super(Settings.Key.DoubleDoors);
}
@Override
protected String title() {
return "Automatische Doppeltüren";
}
@Override
protected String description() {
return "Öffnet und schließt die zweite Hälfte einer Doppeltür automatisch";
}
@Override
protected Material icon() {
return Material.OAK_DOOR;
}
@Override
protected Boolean defaultValue() {
return false;
}
@Override
public SettingCategory category() {
return SettingCategory.Gameplay;
}
}

View File

@ -0,0 +1,27 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.doubleDoor;
import eu.mhsl.craftattack.core.appliance.ApplianceListener;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.event.EventHandler;
import org.bukkit.event.block.Action;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.EquipmentSlot;
import java.util.Objects;
class OnDoorInteractListener extends ApplianceListener<DoubleDoor> {
@EventHandler
public void onPlayerInteract(PlayerInteractEvent event) {
if(!event.hasBlock()) return;
if(!Objects.equals(event.getHand(), EquipmentSlot.HAND)) return;
if(!event.getAction().equals(Action.RIGHT_CLICK_BLOCK)) return;
if(event.getPlayer().isSneaking()) return;
Block clickedBlock = event.getClickedBlock();
if(clickedBlock == null) return;
if(clickedBlock.getType().equals(Material.IRON_DOOR)) return;
if(!Settings.instance().getSetting(event.getPlayer(), Settings.Key.DoubleDoors, Boolean.class)) return;
this.getAppliance().openNextDoor(clickedBlock);
}
}

View File

@ -0,0 +1,25 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.fleischerchest;
import eu.mhsl.craftattack.core.appliance.Appliance;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.TextColor;
import org.bukkit.event.Listener;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public class Fleischerchest extends Appliance {
public void renameItem(ItemStack item) {
ItemMeta meta = item.getItemMeta();
meta.displayName(Component.text("Fleischerchest").color(TextColor.color(235, 20, 28)));
item.setItemMeta(meta);
}
@Override
@NotNull
protected List<Listener> listeners() {
return List.of(new FleischerchestCraftItemListener());
}
}

View File

@ -0,0 +1,18 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.fleischerchest;
import eu.mhsl.craftattack.core.appliance.ApplianceListener;
import org.bukkit.Material;
import org.bukkit.event.EventHandler;
import org.bukkit.event.inventory.PrepareItemCraftEvent;
import org.bukkit.inventory.ItemStack;
class FleischerchestCraftItemListener extends ApplianceListener<Fleischerchest> {
@EventHandler
public void onPrepareItemCraft(PrepareItemCraftEvent event) {
ItemStack result = event.getInventory().getResult();
if(result == null) return;
if(result.getType() != Material.RED_SHULKER_BOX) return;
this.getAppliance().renameItem(event.getInventory().getResult());
}
}

View File

@ -0,0 +1,35 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.glowingBerries;
import eu.mhsl.craftattack.core.appliance.Appliance;
import net.kyori.adventure.sound.Sound;
import net.kyori.adventure.util.Ticks;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public class GlowingBerries extends Appliance {
private static final PotionEffect glowEffect = new PotionEffect(
PotionEffectType.GLOWING,
Ticks.TICKS_PER_SECOND * 15,
1,
false,
true,
true
);
public void letPlayerGlow(Player player) {
player.addPotionEffect(glowEffect);
Sound sound = Sound.sound(org.bukkit.Sound.BLOCK_AMETHYST_BLOCK_CHIME.key(), Sound.Source.PLAYER, 1f, 1f);
player.stopSound(sound);
player.playSound(sound, Sound.Emitter.self());
}
@Override
protected @NotNull List<Listener> listeners() {
return List.of(new OnBerryEaten());
}
}

View File

@ -0,0 +1,14 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.glowingBerries;
import eu.mhsl.craftattack.core.appliance.ApplianceListener;
import org.bukkit.Material;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerItemConsumeEvent;
class OnBerryEaten extends ApplianceListener<GlowingBerries> {
@EventHandler
public void onEat(PlayerItemConsumeEvent event) {
if(event.getItem().getType().equals(Material.GLOW_BERRIES))
this.getAppliance().letPlayerGlow(event.getPlayer());
}
}

View File

@ -0,0 +1,57 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.hotbarRefill;
import eu.mhsl.craftattack.core.Main;
import eu.mhsl.craftattack.core.appliance.Appliance;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.NoSuchElementException;
public class HotbarRefill extends Appliance {
@Override
public void onEnable() {
Settings.instance().declareSetting(HotbarRefillSetting.class);
}
public void handleHotbarChange(Player player, ItemStack item) {
if(player.getGameMode().equals(GameMode.CREATIVE)) return;
if(item.getAmount() != 1) return;
Inventory inventory = player.getInventory();
int itemSlot = inventory.first(item);
if(itemSlot > 8) return;
try {
int replacementSlot = inventory.all(item.getType()).entrySet().stream()
.filter(entry -> entry.getKey() > 8)
.findFirst()
.orElseThrow()
.getKey();
Bukkit.getScheduler().scheduleSyncDelayedTask(Main.instance(), () -> {
ItemStack firstItem = inventory.getItem(itemSlot);
ItemStack secondItem = inventory.getItem(replacementSlot);
inventory.setItem(itemSlot, secondItem);
inventory.setItem(replacementSlot, firstItem);
player.sendActionBar(Component.text("Die Hotbar wurde aufgefüllt", NamedTextColor.GREEN));
}, 1);
} catch(NoSuchElementException ignored) {
}
}
@Override
protected @NotNull List<Listener> listeners() {
return List.of(new HotbarRefillListener());
}
}

View File

@ -0,0 +1,49 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.hotbarRefill;
import eu.mhsl.craftattack.core.appliance.ApplianceListener;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.player.PlayerItemBreakEvent;
import org.bukkit.event.player.PlayerItemConsumeEvent;
import org.bukkit.inventory.ItemStack;
import java.util.List;
class HotbarRefillListener extends ApplianceListener<HotbarRefill> {
private HotbarRefillSetting.HotbarReplaceConfig getPlayerSetting(Player player) {
return Settings.instance().getSetting(
player,
Settings.Key.HotbarReplacer,
HotbarRefillSetting.HotbarReplaceConfig.class
);
}
@EventHandler
public void blockPlace(BlockPlaceEvent event) {
ItemStack stackInHand = event.getItemInHand();
if(stackInHand.getAmount() != 1) return;
if(stackInHand.getType().getMaxDurability() > 0) return;
if(stackInHand.getType().getMaxStackSize() > 0) return;
if(!this.getPlayerSetting(event.getPlayer()).onBlocks()) return;
this.getAppliance().handleHotbarChange(event.getPlayer(), stackInHand);
}
@EventHandler
public void onPlayerItemBreak(PlayerItemBreakEvent event) {
if(!this.getPlayerSetting(event.getPlayer()).onTools()) return;
this.getAppliance().handleHotbarChange(event.getPlayer(), event.getBrokenItem());
}
@EventHandler
public void onPlayerItemConsume(PlayerItemConsumeEvent event) {
if(List.of(Material.POTION, Material.HONEY_BOTTLE).contains(event.getItem().getType())) return;
if(!this.getPlayerSetting(event.getPlayer()).onConsumable()) return;
this.getAppliance().handleHotbarChange(event.getPlayer(), event.getItem());
}
}

View File

@ -0,0 +1,50 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.hotbarRefill;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.CategorizedSetting;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.SettingCategory;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.datatypes.MultiBoolSetting;
import org.bukkit.Material;
public class HotbarRefillSetting extends MultiBoolSetting<HotbarRefillSetting.HotbarReplaceConfig> implements CategorizedSetting {
@Override
public SettingCategory category() {
return SettingCategory.Gameplay;
}
public record HotbarReplaceConfig(
@DisplayName("Blöcke") boolean onBlocks,
@DisplayName("Werkzeuge") boolean onTools,
@DisplayName("Essen") boolean onConsumable
) {
}
public HotbarRefillSetting() {
super(Settings.Key.HotbarReplacer);
}
@Override
protected String title() {
return "Automatische Hotbar";
}
@Override
protected String description() {
return "Verschiebe Items automatisch von deinem Inventar in die Hotbar, wenn diese verbraucht werden";
}
@Override
protected Material icon() {
return Material.CHEST;
}
@Override
protected HotbarReplaceConfig defaultValue() {
return new HotbarReplaceConfig(false, false, false);
}
@Override
public Class<?> dataType() {
return HotbarReplaceConfig.class;
}
}

View File

@ -0,0 +1,65 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.knockDoor;
import eu.mhsl.craftattack.core.Main;
import eu.mhsl.craftattack.core.appliance.Appliance;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.datatypes.SelectSetting;
import org.bukkit.*;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.bukkit.metadata.FixedMetadataValue;
import org.bukkit.scheduler.BukkitRunnable;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public class KnockDoor extends Appliance {
@Override
public void onEnable() {
Settings.instance().declareSetting(KnockDoorSetting.class);
}
public void knockAtDoor(Player knockingPlayer, Block knockedBlock) {
SelectSetting.Options.Option setting = Settings.instance().getSetting(knockingPlayer, Settings.Key.KnockDoors, SelectSetting.Options.Option.class);
if(setting.is(KnockDoorSetting.disabled)) return;
if(setting.is(KnockDoorSetting.knockSingleTime)) {
this.playSound(knockedBlock);
}
if(setting.is(KnockDoorSetting.knockThreeTimes)) {
String metadataKey = new NamespacedKey(Main.instance(), KnockDoor.class.getName()).getNamespace();
if(knockingPlayer.hasMetadata(metadataKey)) return;
knockingPlayer.setMetadata(metadataKey, new FixedMetadataValue(Main.instance(), 0));
new BukkitRunnable() {
@Override
public void run() {
int timesKnocked = knockingPlayer.getMetadata(metadataKey).getFirst().asInt();
if(timesKnocked >= 3) {
knockingPlayer.removeMetadata(metadataKey, Main.instance());
this.cancel();
return;
}
KnockDoor.this.playSound(knockedBlock);
knockingPlayer.setMetadata(metadataKey, new FixedMetadataValue(Main.instance(), timesKnocked + 1));
}
}.runTaskTimer(Main.instance(), 0, 8);
}
}
private void playSound(Block knockedBlock) {
Location knockLocation = knockedBlock.getLocation();
Sound sound = knockedBlock.getType() == Material.IRON_DOOR
? Sound.ENTITY_ZOMBIE_ATTACK_IRON_DOOR
: Sound.ITEM_SHIELD_BLOCK;
knockLocation.getWorld().playSound(knockLocation, sound, SoundCategory.PLAYERS, 1f, 1f);
}
@Override
protected @NotNull List<Listener> listeners() {
return List.of(new KnockDoorListener());
}
}

View File

@ -0,0 +1,18 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.knockDoor;
import eu.mhsl.craftattack.core.appliance.ApplianceListener;
import org.bukkit.GameMode;
import org.bukkit.block.Block;
import org.bukkit.block.data.type.Door;
import org.bukkit.event.EventHandler;
import org.bukkit.event.block.BlockDamageAbortEvent;
class KnockDoorListener extends ApplianceListener<KnockDoor> {
@EventHandler
public void onKnock(BlockDamageAbortEvent event) {
if(event.getPlayer().getGameMode() != GameMode.SURVIVAL) return;
Block block = event.getBlock();
if(!(block.getBlockData() instanceof Door)) return;
this.getAppliance().knockAtDoor(event.getPlayer(), block);
}
}

View File

@ -0,0 +1,50 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.knockDoor;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.CategorizedSetting;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.SettingCategory;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.datatypes.SelectSetting;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import java.util.List;
import java.util.Locale;
public class KnockDoorSetting extends SelectSetting implements CategorizedSetting {
private static final String namespace = KnockDoorSetting.class.getSimpleName().toLowerCase(Locale.ROOT);
public static Options.Option disabled = new Options.Option("Deaktiviert", new NamespacedKey(namespace, "disabled"));
public static Options.Option knockSingleTime = new Options.Option("Einmal an der Tür anklopfen", new NamespacedKey(namespace, "single"));
public static Options.Option knockThreeTimes = new Options.Option("Dreimal an der Tür anklopfen", new NamespacedKey(namespace, "three"));
public KnockDoorSetting() {
super(
Settings.Key.KnockDoors,
new Options(List.of(disabled, knockSingleTime, knockThreeTimes))
);
}
@Override
public SettingCategory category() {
return SettingCategory.Gameplay;
}
@Override
protected String title() {
return "Klopfen an Türen";
}
@Override
protected String description() {
return "Klopft durch das schlagen an eine Tür an.";
}
@Override
protected Material icon() {
return Material.BELL;
}
@Override
protected Options.Option defaultValue() {
return disabled;
}
}

View File

@ -0,0 +1,7 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.outlawed;
class OutlawChangeNotPermitted extends Exception {
public OutlawChangeNotPermitted(String message) {
super(message);
}
}

View File

@ -0,0 +1,134 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.outlawed;
import eu.mhsl.craftattack.core.Main;
import eu.mhsl.craftattack.core.appliance.Appliance;
import eu.mhsl.craftattack.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.displayName.DisplayName;
import eu.mhsl.craftattack.spawn.craftattack.appliances.tooling.whitelist.Whitelist;
import eu.mhsl.craftattack.core.config.Configuration;
import eu.mhsl.craftattack.core.util.text.DisconnectInfo;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.jetbrains.annotations.NotNull;
import java.util.*;
public class Outlawed extends Appliance implements DisplayName.Prefixed {
public final int timeoutInMs = 1000 * 60 * 60 * 6;
private final Map<UUID, Long> timeouts = new HashMap<>();
public enum Status {
DISABLED,
VOLUNTARILY,
FORCED
}
private final Map<Player, Status> playerStatusMap = new WeakHashMap<>();
private final String voluntarilyEntry = "voluntarily";
public Outlawed() {
super("outlawed");
Bukkit.getScheduler().runTaskTimerAsynchronously(
Main.instance(),
() -> this.playerStatusMap.forEach((player, status) -> {
if(!player.isOnline()) return;
if(status != Status.FORCED) return;
try {
this.queryAppliance(Whitelist.class).fullIntegrityCheck(player);
} catch(DisconnectInfo.Throwable e) {
e.getDisconnectScreen().applyKick(player);
}
}),
20 * 60,
20 * 60 * 5
);
}
public void switchLawStatus(Player player) throws OutlawChangeNotPermitted {
if(this.getLawStatus(player).equals(Status.FORCED)) {
throw new OutlawChangeNotPermitted("Dein Vogelfreistatus wurde als Strafe auferlegt und kann daher nicht verändert werden.");
}
if(this.isTimeout(player)) {
throw new OutlawChangeNotPermitted("Du kannst deinen Vogelfreistatus nicht so schnell wechseln. Bitte warte einige Stunden bevor du umschaltest!");
}
this.setLawStatus(player, this.isOutlawed(player) ? Status.DISABLED : Status.VOLUNTARILY);
this.setTimeout(player);
}
public void updateForcedStatus(Player player, boolean forced) {
this.setLawStatus(player, forced ? Status.FORCED : this.getLawStatus(player) == Status.FORCED ? Status.DISABLED : this.getLawStatus(player));
}
public Status getLawStatus(Player player) {
return this.playerStatusMap.computeIfAbsent(player, p -> {
if(this.localConfig().getStringList(this.voluntarilyEntry).contains(p.getUniqueId().toString()))
return Status.VOLUNTARILY;
return Status.DISABLED;
});
}
private void setLawStatus(Player player, Status status) {
this.playerStatusMap.put(player, status);
this.queryAppliance(DisplayName.class).update(player);
List<String> newList = this.localConfig().getStringList(this.voluntarilyEntry);
if(status.equals(Status.VOLUNTARILY)) {
newList.add(player.getUniqueId().toString());
} else {
newList.remove(player.getUniqueId().toString());
}
this.localConfig().set(this.voluntarilyEntry, newList.stream().distinct().toList());
Configuration.saveChanges();
}
public boolean isOutlawed(Player player) {
return this.getLawStatus(player) != Status.DISABLED;
}
private boolean isTimeout(Player player) {
return this.timeouts.getOrDefault(player.getUniqueId(), 0L) > System.currentTimeMillis() - this.timeoutInMs;
}
private void setTimeout(Player player) {
this.timeouts.put(player.getUniqueId(), System.currentTimeMillis());
}
public Component getStatusDescription(Status status) {
return switch(status) {
case DISABLED -> Component.text("Vogelfreistatus inaktiv: ", NamedTextColor.GREEN)
.append(Component.text("Es gelten die Standard Regeln!", NamedTextColor.GOLD));
case VOLUNTARILY, FORCED -> Component.text("Vogelfreistatus aktiv: ", NamedTextColor.RED)
.append(Component.text("Du darfst von jedem angegriffen und getötet werden!", NamedTextColor.GOLD));
};
}
@Override
public Component getNamePrefix(Player player) {
if(this.isOutlawed(player)) {
return Component.text("[☠]", NamedTextColor.RED)
.hoverEvent(HoverEvent.showText(Component.text("Vogelfreie Spieler dürfen ohne Grund angegriffen werden!")));
}
return null;
}
@Override
@NotNull
protected List<ApplianceCommand<?>> commands() {
return List.of(new OutlawedCommand());
}
@Override
@NotNull
protected List<Listener> listeners() {
return List.of(new OutlawedReminderListener());
}
}

View File

@ -0,0 +1,27 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.outlawed;
import eu.mhsl.craftattack.core.appliance.ApplianceCommand;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
class OutlawedCommand extends ApplianceCommand.PlayerChecked<Outlawed> {
public OutlawedCommand() {
super("vogelfrei");
}
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception {
try {
this.getAppliance().switchLawStatus(this.getPlayer());
sender.sendMessage(
this.getAppliance()
.getStatusDescription(this.getAppliance().getLawStatus(this.getPlayer()))
);
} catch(OutlawChangeNotPermitted e) {
sender.sendMessage(Component.text(e.getMessage(), NamedTextColor.RED));
}
}
}

View File

@ -0,0 +1,14 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.outlawed;
import eu.mhsl.craftattack.core.appliance.ApplianceListener;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerJoinEvent;
class OutlawedReminderListener extends ApplianceListener<Outlawed> {
@EventHandler
public void onJoin(PlayerJoinEvent e) {
if(this.getAppliance().isOutlawed(e.getPlayer())) {
e.getPlayer().sendMessage(this.getAppliance().getStatusDescription(this.getAppliance().getLawStatus(e.getPlayer())));
}
}
}

View File

@ -0,0 +1,16 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.portableCrafting;
import eu.mhsl.craftattack.core.appliance.ApplianceListener;
import org.bukkit.Material;
import org.bukkit.event.EventHandler;
import org.bukkit.event.block.Action;
import org.bukkit.event.player.PlayerInteractEvent;
class OnCraftingTableUseListener extends ApplianceListener<PortableCrafting> {
@EventHandler
public void inInteract(PlayerInteractEvent event) {
if(!event.getAction().equals(Action.RIGHT_CLICK_AIR)) return;
if(!event.getMaterial().equals(Material.CRAFTING_TABLE)) return;
this.getAppliance().openFor(event.getPlayer());
}
}

View File

@ -0,0 +1,26 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.portableCrafting;
import eu.mhsl.craftattack.core.appliance.Appliance;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public class PortableCrafting extends Appliance {
@Override
public void onEnable() {
Settings.instance().declareSetting(PortableCraftingSetting.class);
}
public void openFor(Player player) {
if(!Settings.instance().getSetting(player, Settings.Key.EnablePortableCrafting, Boolean.class)) return;
player.openWorkbench(null, true);
}
@Override
protected @NotNull List<Listener> listeners() {
return List.of(new OnCraftingTableUseListener());
}
}

View File

@ -0,0 +1,38 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.portableCrafting;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.CategorizedSetting;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.SettingCategory;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.datatypes.BoolSetting;
import org.bukkit.Material;
public class PortableCraftingSetting extends BoolSetting implements CategorizedSetting {
public PortableCraftingSetting() {
super(Settings.Key.EnablePortableCrafting);
}
@Override
protected String title() {
return "Portables Crafting";
}
@Override
protected String description() {
return "Erlaubt das öffnen einer Werkbank in der Hand, ohne sie plazieren zu müssen";
}
@Override
protected Material icon() {
return Material.CRAFTING_TABLE;
}
@Override
protected Boolean defaultValue() {
return true;
}
@Override
public SettingCategory category() {
return SettingCategory.Gameplay;
}
}

View File

@ -0,0 +1,22 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.snowballKnockback;
import eu.mhsl.craftattack.core.appliance.Appliance;
import org.bukkit.entity.Entity;
import org.bukkit.entity.LivingEntity;
import org.bukkit.event.Listener;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public class SnowballKnockback extends Appliance {
public void dealSnowballKnockback(LivingEntity entity, Entity snowball) {
entity.damage(0.1);
entity.knockback(0.4, -snowball.getVelocity().getX(), -snowball.getVelocity().getZ());
}
@Override
@NotNull
protected List<Listener> listeners() {
return List.of(new SnowballKnockbackListener());
}
}

View File

@ -0,0 +1,20 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.snowballKnockback;
import eu.mhsl.craftattack.core.appliance.ApplianceListener;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.LivingEntity;
import org.bukkit.event.EventHandler;
import org.bukkit.event.entity.ProjectileHitEvent;
class SnowballKnockbackListener extends ApplianceListener<SnowballKnockback> {
@EventHandler
public void onSnowballHit(ProjectileHitEvent event) {
if(event.getHitEntity() == null) return;
if(!event.getEntityType().equals(EntityType.SNOWBALL)) return;
if(!(event.getHitEntity() instanceof LivingEntity hitEntity)) return;
Entity snowball = event.getEntity();
this.getAppliance().dealSnowballKnockback(hitEntity, snowball);
}
}

View File

@ -0,0 +1,14 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.adminMarker;
import eu.mhsl.craftattack.core.appliance.Appliance;
import net.kyori.adventure.text.format.TextColor;
import org.bukkit.Color;
import org.bukkit.entity.Player;
public class AdminMarker extends Appliance {
public TextColor getPlayerColor(Player player) {
if(player.hasPermission("chatcolor"))
return TextColor.color(Color.AQUA.asRGB()); // TODO read permission from config
return TextColor.color(Color.WHITE.asRGB());
}
}

View File

@ -0,0 +1,30 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.afkTag;
import eu.mhsl.craftattack.core.appliance.ApplianceListener;
import io.papermc.paper.event.player.AsyncChatEvent;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerMoveEvent;
class AfkResetListener extends ApplianceListener<AfkTag> {
@EventHandler
public void onMove(PlayerMoveEvent event) {
this.getAppliance().resetTiming(event.getPlayer());
}
@EventHandler
public void onInteract(PlayerInteractEvent event) {
this.getAppliance().resetTiming(event.getPlayer());
}
@EventHandler
public void onChat(AsyncChatEvent event) {
this.getAppliance().resetTiming(event.getPlayer());
}
@EventHandler
public void onJoin(PlayerJoinEvent event) {
this.getAppliance().resetTiming(event.getPlayer());
}
}

View File

@ -0,0 +1,74 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.afkTag;
import eu.mhsl.craftattack.core.Main;
import eu.mhsl.craftattack.core.appliance.Appliance;
import eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.displayName.DisplayName;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.util.Ticks;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
public class AfkTag extends Appliance implements DisplayName.Prefixed {
private final HashMap<UUID, Long> afkTimings = new HashMap<>();
private static final int updateIntervalSeconds = 30;
private static final int afkWhenMillis = 3 * 60 * 1000;
@Override
public void onEnable() {
Bukkit.getScheduler().runTaskTimerAsynchronously(
Main.instance(),
this::checkAfkPlayers,
Ticks.TICKS_PER_SECOND,
Ticks.TICKS_PER_SECOND * updateIntervalSeconds
);
}
public void resetTiming(Player player) {
boolean wasAfk = this.isAfk(player);
this.afkTimings.put(player.getUniqueId(), System.currentTimeMillis());
if(wasAfk) this.updateAfkPrefix(player);
}
private void checkAfkPlayers() {
this.afkTimings.keySet().stream()
.map(Bukkit::getPlayer)
.filter(Objects::nonNull)
.filter(this::isAfk)
.forEach(this::updateAfkPrefix);
}
private boolean isAfk(Player player) {
if(player.isSleeping()) return false;
long lastTimeActive = this.afkTimings.getOrDefault(player.getUniqueId(), 0L);
long timeSinceLastActive = System.currentTimeMillis() - lastTimeActive;
return timeSinceLastActive >= afkWhenMillis;
}
private void updateAfkPrefix(Player player) {
Main.instance().getAppliance(DisplayName.class).update(player);
}
@Override
public @Nullable Component getNamePrefix(Player player) {
if(this.isAfk(player)) return Component.text("[ᵃᶠᵏ]", NamedTextColor.GRAY)
.hoverEvent(HoverEvent.showText(Component.text("Der Spieler ist AFK")));
return null;
}
@Override
protected @NotNull List<Listener> listeners() {
return List.of(new AfkResetListener());
}
}

View File

@ -0,0 +1,75 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.displayName;
import eu.mhsl.craftattack.core.Main;
import eu.mhsl.craftattack.core.appliance.Appliance;
import eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.outlawed.Outlawed;
import eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.adminMarker.AdminMarker;
import eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.afkTag.AfkTag;
import eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.sleepTag.SleepTag;
import eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.yearRank.YearRank;
import eu.mhsl.craftattack.core.util.server.Floodgate;
import eu.mhsl.craftattack.core.util.text.ComponentUtil;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentBuilder;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Objects;
import java.util.function.Supplier;
import java.util.logging.Level;
public class DisplayName extends Appliance {
public interface Prefixed {
@Nullable
Component getNamePrefix(Player player);
}
public void update(Player player) {
TextColor playerColor = this.queryAppliance(AdminMarker.class).getPlayerColor(player);
List<Supplier<Prefixed>> prefixes = List.of(
() -> this.queryAppliance(Outlawed.class),
() -> this.queryAppliance(YearRank.class),
() -> this.queryAppliance(AfkTag.class),
() -> this.queryAppliance(SleepTag.class)
);
ComponentBuilder<TextComponent, TextComponent.Builder> playerName = Component.text();
prefixes.stream()
.map(prefixed -> prefixed.get().getNamePrefix(player))
.filter(Objects::nonNull)
.forEach(prefix -> playerName
.append(prefix)
.append(ComponentUtil.clearedSpace())
);
if(Floodgate.isBedrock(player)) {
playerName
.append(
Component.text("\uD83C\uDFAE", NamedTextColor.GRAY)
.hoverEvent(HoverEvent.showText(Component.text(
String.format("%s spielt die Minecraft: Bedrock Edition", player.getName())
)))
)
.append(ComponentUtil.clearedSpace());
}
playerName.append(Component.text(player.getName(), playerColor));
this.setGlobal(player, playerName.build());
}
private void setGlobal(Player player, Component component) {
try {
player.customName(component);
player.displayName(component);
player.playerListName(component);
} catch(Exception e) {
Main.instance().getLogger().log(Level.SEVERE, e, e::getMessage);
}
}
}

View File

@ -0,0 +1,13 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.displayName;
import eu.mhsl.craftattack.core.appliance.ApplianceListener;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.player.PlayerJoinEvent;
class DisplayNameUpdateListener extends ApplianceListener<DisplayName> {
@EventHandler(priority = EventPriority.LOW)
public void onJoin(PlayerJoinEvent event) {
this.getAppliance().update(event.getPlayer());
}
}

View File

@ -0,0 +1,12 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.event;
import eu.mhsl.craftattack.core.appliance.ApplianceListener;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerJoinEvent;
class ApplyPendingRewardsListener extends ApplianceListener<Event> {
@EventHandler
public void onJoin(PlayerJoinEvent event) {
this.getAppliance().applyPendingRewards(event.getPlayer());
}
}

View File

@ -0,0 +1,230 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.event;
import eu.mhsl.craftattack.core.Main;
import eu.mhsl.craftattack.core.api.client.ReqResp;
import eu.mhsl.craftattack.core.api.client.repositories.EventRepository;
import eu.mhsl.craftattack.core.api.server.HttpServer;
import eu.mhsl.craftattack.core.appliance.Appliance;
import eu.mhsl.craftattack.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.customAdvancements.Advancements;
import eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.customAdvancements.CustomAdvancements;
import eu.mhsl.craftattack.core.util.IteratorUtil;
import eu.mhsl.craftattack.core.util.api.HttpStatus;
import eu.mhsl.craftattack.core.util.entity.DisplayVillager;
import eu.mhsl.craftattack.core.util.listener.DismissInventoryOpenFromHolder;
import eu.mhsl.craftattack.core.util.listener.PlayerInteractAtEntityEventListener;
import eu.mhsl.craftattack.core.util.server.PluginMessage;
import eu.mhsl.craftattack.core.util.text.ComponentUtil;
import eu.mhsl.craftattack.core.util.text.Countdown;
import eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.event.command.*;
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.entity.Villager;
import org.bukkit.event.Listener;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.jetbrains.annotations.NotNull;
import java.util.*;
public class Event extends Appliance {
enum AdvertisementStatus {
BEFORE,
ADVERTISED,
DONE
}
Countdown advertiseCountdown = new Countdown(
120,
announcementData -> Component.text()
.append(ComponentUtil.createRainbowText("Event", 30))
.append(Component.text(" Start in ", NamedTextColor.GOLD))
.append(Component.text(announcementData.count(), NamedTextColor.AQUA))
.append(Component.text(" " + announcementData.unit() + "!", NamedTextColor.GOLD))
.build(),
component -> IteratorUtil.onlinePlayers(player -> player.sendMessage(component)),
() -> this.advertiseStatus = AdvertisementStatus.DONE
);
public DisplayVillager.ConfigBound villager;
private boolean isOpen = false;
private AdvertisementStatus advertiseStatus = AdvertisementStatus.BEFORE;
private UUID roomId;
private final List<Reward> pendingRewards = new ArrayList<>();
record RewardConfiguration(String memorialMaterial, String memorialTitle, String memorialLore, List<UUID> memorials,
String material, Map<UUID, Integer> rewards) {
}
record Reward(UUID playerUuid, ItemStack itemStack) {
}
public Event() {
super("event");
}
@Override
public void onEnable() {
this.villager = new DisplayVillager.ConfigBound(
this.localConfig(),
villager -> {
villager.customName(Component.text("Events", NamedTextColor.GOLD));
villager.setProfession(Villager.Profession.LIBRARIAN);
villager.setVillagerType(Villager.Type.SNOW);
}
);
this.isOpen = this.localConfig().getBoolean("enabled", false);
if(this.isOpen) this.roomId = UUID.fromString(this.localConfig().getString("roomId", ""));
}
public void openEvent() {
if(this.isOpen) throw new ApplianceCommand.Error("Es läuft derzeit bereits ein Event!");
Bukkit.getScheduler().runTaskAsynchronously(Main.instance(), () -> {
ReqResp<EventRepository.CreatedRoom> sessionResponse = this.queryRepository(EventRepository.class).createSession();
if(sessionResponse.status() != HttpStatus.OK)
throw new ApplianceCommand.Error("Event-Server meldet Fehler: " + sessionResponse.status());
this.isOpen = true;
this.roomId = sessionResponse.data().uuid();
});
}
public void joinEvent(Player p) {
if(!this.isOpen) {
p.sendMessage(Component.text("Zurzeit ist kein Event geöffnet.", NamedTextColor.RED));
return;
}
if(!p.hasPermission("admin") && this.advertiseStatus == AdvertisementStatus.BEFORE) {
p.sendMessage(Component.text("Die Event befinden sich noch in der Vorbereitung.", NamedTextColor.RED));
return;
}
if(!p.hasPermission("admin") && this.advertiseStatus == AdvertisementStatus.DONE) {
p.sendMessage(Component.text("Die Events laufen bereits. Ein nachträgliches Beitreten ist nicht möglich.", NamedTextColor.RED));
return;
}
Main.instance().getLogger().info("Verbinde mit eventserver: " + p.getName());
p.sendMessage(Component.text("Authentifiziere...", NamedTextColor.GREEN));
Bukkit.getScheduler().runTaskAsynchronously(Main.instance(), () -> {
ReqResp<EventRepository.QueueRoom.Response> queueResponse = this.queryRepository(EventRepository.class)
.queueRoom(new EventRepository.QueueRoom(p.getUniqueId(), this.roomId));
if(queueResponse.status() != HttpStatus.OK || queueResponse.data().error() != null) {
p.sendMessage(Component.text("Fehler beim Betreten: " + queueResponse.data().error(), NamedTextColor.RED));
return;
}
p.sendMessage(Component.text("Betrete...", NamedTextColor.GREEN));
PluginMessage.connect(p, this.localConfig().getString("connect-server-name"));
});
}
public void endEvent() {
if(!this.isOpen) throw new ApplianceCommand.Error("Es läuft derzeit kein Event!");
this.isOpen = false;
}
private void rewardPlayers(RewardConfiguration rewardConfiguration) {
rewardConfiguration.rewards.forEach((uuid, amount) -> {
Reward reward = new Reward(uuid, new ItemStack(Objects.requireNonNull(Material.matchMaterial(rewardConfiguration.material)), amount));
if(Bukkit.getPlayer(uuid) == null) {
this.pendingRewards.add(reward);
return;
}
this.giveReward(reward);
});
rewardConfiguration.memorials.forEach(uuid -> {
ItemStack memorialItem = new ItemStack(Objects.requireNonNull(Material.matchMaterial(rewardConfiguration.memorialMaterial)));
ItemMeta meta = memorialItem.getItemMeta();
meta.displayName(Component.text(rewardConfiguration.memorialTitle, NamedTextColor.GOLD));
meta.lore(List.of(Component.text(rewardConfiguration.memorialLore, NamedTextColor.AQUA)));
memorialItem.setItemMeta(meta);
Reward memorial = new Reward(uuid, memorialItem);
Main.instance().getAppliance(CustomAdvancements.class).grantAdvancement(Advancements.participateEvent, uuid);
if(Bukkit.getPlayer(uuid) == null) {
this.pendingRewards.add(memorial);
return;
}
this.giveReward(memorial);
});
rewardConfiguration.rewards.keySet().stream()
.max(Comparator.comparing(rewardConfiguration.rewards::get))
.ifPresent(uuid -> Main.instance().getAppliance(CustomAdvancements.class)
.grantAdvancement(Advancements.winner, uuid));
}
private void giveReward(Reward reward) {
Player player = Bukkit.getPlayer(reward.playerUuid);
if(player == null) throw new RuntimeException("Cannot reward offline playerUuid!");
Map<Integer, ItemStack> remaining = player.getInventory().addItem(reward.itemStack);
Bukkit.getScheduler().runTask(
Main.instance(),
() -> remaining.values().forEach(remainingStack -> player.getWorld().dropItem(player.getLocation(), remainingStack))
);
}
public void applyPendingRewards(Player player) {
if(this.pendingRewards.isEmpty()) return;
List<Reward> givenRewards = this.pendingRewards.stream().filter(reward -> reward.playerUuid.equals(player.getUniqueId())).toList();
this.pendingRewards.removeAll(givenRewards);
givenRewards.forEach(this::giveReward);
}
public void advertise() {
this.advertiseCountdown.cancelIfRunning();
this.advertiseStatus = AdvertisementStatus.ADVERTISED;
IteratorUtil.onlinePlayers(player -> player.sendMessage(
Component.text()
.append(Component.text("-".repeat(10), NamedTextColor.GRAY)).appendNewline()
.append(Component.text("Ein Event wurde gestartet!", NamedTextColor.GOLD)).appendNewline()
.append(Component.text("Nutze "))
.append(Component.text("/event", NamedTextColor.AQUA))
.append(Component.text(", um dem Event beizutreten!")).appendNewline()
.append(Component.text("-".repeat(10), NamedTextColor.GRAY)).appendNewline()
));
this.advertiseCountdown.start();
}
@Override
public void httpApi(HttpServer.ApiBuilder apiBuilder) {
apiBuilder.post("reward", RewardConfiguration.class, (rewardConfiguration, request) -> {
this.rewardPlayers(rewardConfiguration);
return HttpServer.nothing;
});
}
@Override
@NotNull
protected List<ApplianceCommand<?>> commands() {
return List.of(
new EventCommand(),
new MoveEventVillagerCommand(),
new EventOpenSessionCommand(),
new EventEndSessionCommand(),
new EventAdvertiseCommand()
);
}
@Override
@NotNull
protected List<Listener> listeners() {
return List.of(
new ApplyPendingRewardsListener(),
new PlayerInteractAtEntityEventListener(this.villager.getUniqueId(), playerInteractAtEntityEvent -> this.joinEvent(playerInteractAtEntityEvent.getPlayer())),
new DismissInventoryOpenFromHolder(this.villager.getUniqueId())
);
}
}

View File

@ -0,0 +1,18 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.event.command;
import eu.mhsl.craftattack.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.event.Event;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
public class EventAdvertiseCommand extends ApplianceCommand<Event> {
public EventAdvertiseCommand() {
super("eventAdvertise");
}
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception {
this.getAppliance().advertise();
}
}

View File

@ -0,0 +1,18 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.event.command;
import eu.mhsl.craftattack.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.event.Event;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
public class EventCommand extends ApplianceCommand.PlayerChecked<Event> {
public EventCommand() {
super("event");
}
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception {
this.getAppliance().joinEvent(this.getPlayer());
}
}

View File

@ -0,0 +1,18 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.event.command;
import eu.mhsl.craftattack.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.event.Event;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
public class EventEndSessionCommand extends ApplianceCommand<Event> {
public EventEndSessionCommand() {
super("eventEndSession");
}
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
this.getAppliance().endEvent();
}
}

View File

@ -0,0 +1,21 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.event.command;
import eu.mhsl.craftattack.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.event.Event;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
public class EventOpenSessionCommand extends ApplianceCommand<Event> {
public EventOpenSessionCommand() {
super("eventOpenSession");
}
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception {
this.getAppliance().openEvent();
sender.sendMessage(Component.text("Event-Server gestartet!", NamedTextColor.GREEN));
}
}

View File

@ -0,0 +1,18 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.event.command;
import eu.mhsl.craftattack.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.event.Event;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
public class MoveEventVillagerCommand extends ApplianceCommand.PlayerChecked<Event> {
public MoveEventVillagerCommand() {
super("moveEventVillager");
}
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
this.getAppliance().villager.updateLocation(this.getPlayer().getLocation());
}
}

View File

@ -0,0 +1,77 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.feedback;
import eu.mhsl.craftattack.core.Main;
import eu.mhsl.craftattack.core.api.client.ReqResp;
import eu.mhsl.craftattack.core.api.client.repositories.FeedbackRepository;
import eu.mhsl.craftattack.core.appliance.Appliance;
import eu.mhsl.craftattack.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.core.util.api.HttpStatus;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentBuilder;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Map;
import java.util.UUID;
public class Feedback extends Appliance {
public Feedback() {
super("feedback");
}
public void requestFeedback(String eventName, List<Player> receivers, @Nullable String question) {
ReqResp<Map<UUID, String>> response = this.queryRepository(FeedbackRepository.class).createFeedbackUrls(
new FeedbackRepository.Request(eventName, receivers.stream().map(Entity::getUniqueId).toList())
);
System.out.println(response.toString());
System.out.println(response.status());
if(response.status() != HttpStatus.CREATED) throw new RuntimeException();
Component border = Component.text("-".repeat(40), NamedTextColor.GRAY);
receivers.forEach(player -> {
String feedbackUrl = response.data().get(player.getUniqueId());
if(feedbackUrl == null) {
Main.logger().warning(String.format("FeedbackUrl not found for player '%s' from backend!", player.getUniqueId()));
return;
}
ComponentBuilder<TextComponent, TextComponent.Builder> message = Component.text()
.append(border)
.appendNewline();
if(question != null) {
message
.append(Component.text(question, NamedTextColor.GREEN))
.appendNewline()
.appendNewline();
}
message
.append(Component.text("Klicke hier und gib uns Feedback, damit wir dein Spielerlebnis verbessern können!", NamedTextColor.DARK_GREEN)
.clickEvent(ClickEvent.openUrl(feedbackUrl)))
.hoverEvent(HoverEvent.showText(Component.text("Klicke, um Feedback zu geben.")))
.appendNewline()
.append(border);
player.sendMessage(message.build());
});
}
@Override
protected @NotNull List<ApplianceCommand<?>> commands() {
return List.of(
new FeedbackCommand(),
new RequestFeedbackCommand()
);
}
}

View File

@ -0,0 +1,30 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.feedback;
import eu.mhsl.craftattack.core.Main;
import eu.mhsl.craftattack.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.core.util.text.ComponentUtil;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import java.util.List;
class FeedbackCommand extends ApplianceCommand.PlayerChecked<Feedback> {
public FeedbackCommand() {
super("feedback");
}
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception {
sender.sendMessage(ComponentUtil.pleaseWait());
Bukkit.getScheduler().runTaskAsynchronously(
Main.instance(),
() -> this.getAppliance().requestFeedback(
"self-issued-ingame",
List.of(this.getPlayer()),
null
)
);
}
}

View File

@ -0,0 +1,27 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.feedback;
import eu.mhsl.craftattack.core.Main;
import eu.mhsl.craftattack.core.appliance.ApplianceCommand;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
class RequestFeedbackCommand extends ApplianceCommand<Feedback> {
public RequestFeedbackCommand() {
super("requestFeedback");
}
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception {
Bukkit.getScheduler().runTaskAsynchronously(
Main.instance(),
() -> this.getAppliance().requestFeedback(
"admin-issued-ingame",
new ArrayList<>(Bukkit.getOnlinePlayers()), String.join(" ", args)
)
);
}
}

View File

@ -0,0 +1,70 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.optionLinks;
import eu.mhsl.craftattack.core.Main;
import eu.mhsl.craftattack.core.appliance.Appliance;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.bukkit.ServerLinks;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.jetbrains.annotations.NotNull;
import java.net.URI;
import java.util.List;
import java.util.function.Function;
import java.util.logging.Level;
@SuppressWarnings("UnstableApiUsage")
public class OptionLinks extends Appliance {
record ComponentSupplier() {
}
record UriSupplier(Player player) {
}
record UriConsumer(String uri) {
}
record SuppliedLink(Function<ComponentSupplier, Component> component, Function<UriSupplier, UriConsumer> uri) {
}
List<SuppliedLink> links = List.of(
new SuppliedLink(
componentSupplier -> Component.text("CraftAttack Homepage", NamedTextColor.GOLD),
uriSupplier -> new UriConsumer("https://mhsl.eu/craftattack")
),
new SuppliedLink(
componentSupplier -> Component.text("Regeln", NamedTextColor.GOLD),
uriSupplier -> new UriConsumer("https://mhsl.eu/craftattack/rules")
)
);
@Override
public void onEnable() {
Bukkit.getServer().getServerLinks().getLinks()
.forEach(serverLink -> Bukkit.getServer().getServerLinks().removeLink(serverLink));
}
public void setServerLinks(Player player) {
ServerLinks playerLinks = Bukkit.getServerLinks().copy();
Bukkit.getScheduler().runTaskAsynchronously(Main.instance(), () -> {
this.links.forEach(suppliedLink -> {
Component component = suppliedLink.component.apply(new ComponentSupplier());
String uri = suppliedLink.uri.apply(new UriSupplier(player)).uri;
try {
playerLinks.addLink(component, URI.create(uri));
} catch(IllegalArgumentException e) {
Main.logger().log(Level.INFO, String.format("Failed to create OptionLink '%s' for player '%s'", uri, player.getName()), e);
}
});
player.sendLinks(playerLinks);
});
}
@Override
protected @NotNull List<Listener> listeners() {
return List.of(new UpdateLinksListener());
}
}

View File

@ -0,0 +1,12 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.optionLinks;
import eu.mhsl.craftattack.core.appliance.ApplianceListener;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerJoinEvent;
class UpdateLinksListener extends ApplianceListener<OptionLinks> {
@EventHandler
public void onJoin(PlayerJoinEvent event) {
this.getAppliance().setServerLinks(event.getPlayer());
}
}

View File

@ -0,0 +1,19 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.packSelect;
import eu.mhsl.craftattack.core.appliance.ApplianceCommand;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
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 {
this.getAppliance().openPackInventory(this.getPlayer());
}
}

View File

@ -0,0 +1,99 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.packSelect;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import eu.mhsl.craftattack.core.appliance.CachedApplianceSupplier;
import eu.mhsl.craftattack.core.util.inventory.HeadBuilder;
import eu.mhsl.craftattack.core.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(this.icon);
ItemMeta meta = stack.getItemMeta();
meta.itemName(Component.text(this.id.toString()));
meta.displayName(Component.text(this.name(), NamedTextColor.GOLD));
List<Component> lore = new ArrayList<>();
lore.add(Component.text("Autor: ", NamedTextColor.DARK_GRAY).append(Component.text(this.author)));
lore.add(Component.text(" "));
lore.addAll(
ComponentUtil.lineBreak(this.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(this.id);
} catch(IllegalArgumentException ignored) {
return false;
}
}
}
public record PackList(List<Pack> packs) {
public Optional<Pack> findFromItem(ItemStack itemStack) {
return this.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 = this.gson.fromJson(data, SerializedPackList.class);
var availablePackMap = this.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 this.gson.toJson(new SerializedPackList(this.packList.packs().stream().map(Pack::id).toList()));
}
public List<Pack> getPackList() {
return this.packList.packs();
}
}

View File

@ -0,0 +1,171 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.packSelect;
import eu.mhsl.craftattack.core.Main;
import eu.mhsl.craftattack.core.appliance.CachedApplianceSupplier;
import eu.mhsl.craftattack.core.util.IteratorUtil;
import eu.mhsl.craftattack.core.util.inventory.ItemBuilder;
import eu.mhsl.craftattack.core.util.inventory.PlaceholderItems;
import eu.mhsl.craftattack.core.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 this.inventory;
}
public void handleClick(@Nullable ItemStack clickedItem, int slot, ClickType clickType) {
if(clickedItem == null) return;
if(clickedItem.equals(this.reset)) this.reset();
if(clickedItem.equals(this.save)) this.apply();
if(slot >= 9 && slot < 9 * 4) {
this.getAppliance().availablePacks.findFromItem(clickedItem)
.ifPresent(this::toggle);
}
if(slot >= 9 * 5) {
this.getAppliance().availablePacks.findFromItem(clickedItem)
.ifPresent(pack -> {
switch(clickType) {
case RIGHT -> this.move(pack, true);
case LEFT -> this.move(pack, false);
case MIDDLE -> this.toggle(pack);
}
});
}
}
private void move(PackConfiguration.Pack pack, boolean moveToRight) {
List<PackConfiguration.Pack> packs = this.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(this.inventoryOwner).click();
this.draw();
}
private void toggle(PackConfiguration.Pack pack) {
if(this.packConfiguration.getPackList().contains(pack)) {
this.packConfiguration.getPackList().remove(pack);
} else {
this.packConfiguration.getPackList().add(pack);
}
InteractSounds.of(this.inventoryOwner).click();
this.draw();
}
private void reset() {
this.packConfiguration.getPackList().clear();
InteractSounds.of(this.inventoryOwner).delete();
this.draw();
}
private void apply() {
this.inventoryOwner.closeInventory();
InteractSounds.of(this.inventoryOwner).success();
Bukkit.getScheduler().runTask(Main.instance(), () -> this.getAppliance().setPack(this.inventoryOwner, this.packConfiguration));
}
private void draw() {
this.inventory.clear();
this.inventory.setItem(0, this.packConfiguration.getPackList().isEmpty() ? PlaceholderItems.grayStainedGlassPane : this.reset);
IteratorUtil.times(3, () -> this.inventory.addItem(PlaceholderItems.grayStainedGlassPane));
this.inventory.setItem(4, this.info);
IteratorUtil.times(3, () -> this.inventory.addItem(PlaceholderItems.grayStainedGlassPane));
this.inventory.setItem(8, this.save);
IteratorUtil.iterateListInGlobal(
9,
this.getAppliance().availablePacks.packs().stream()
.filter(pack -> !this.packConfiguration.getPackList().contains(pack))
.limit(9 * 3)
.map(pack -> {
ItemBuilder stack = ItemBuilder.of(pack.buildItem());
if(this.packConfiguration.getPackList().contains(pack)) stack.glint();
return stack.build();
})
.toList(),
this.inventory::setItem
);
IntStream.range(9 * 4, 9 * 5)
.forEach(slot -> this.inventory.setItem(slot, PlaceholderItems.grayStainedGlassPane));
IteratorUtil.iterateListInGlobal(
9 * 5,
IteratorUtil.expandList(
IntStream.range(0, Math.min(this.packConfiguration.getPackList().size(), 9))
.mapToObj(i -> {
PackConfiguration.Pack pack = this.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,
this.unusedSlot
),
this.inventory::setItem
);
}
}

View File

@ -0,0 +1,139 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.packSelect;
import eu.mhsl.craftattack.core.Main;
import eu.mhsl.craftattack.core.appliance.Appliance;
import eu.mhsl.craftattack.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.packSelect.listeners.ClickPackInventoryListener;
import eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.packSelect.listeners.ClosePackInventoryListener;
import eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.packSelect.listeners.SetPacksOnJoinListener;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
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() {
Settings.instance().declareSetting(PackSelectSetting.class);
List<Map<?, ?>> packs = this.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")
);
this.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(this.getPackConfigurationForPlayer(player), player);
player.openInventory(packInventory.getInventory());
this.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,36 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.packSelect;
import eu.mhsl.craftattack.core.Main;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.CategorizedSetting;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.SettingCategory;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.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

@ -0,0 +1,62 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.packSelect;
import eu.mhsl.craftattack.core.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;
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.craftattack.appliances.metaGameplay.packSelect.listeners;
import eu.mhsl.craftattack.core.appliance.ApplianceListener;
import eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.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(this.getAppliance().isNotPackInventory(player, event.getInventory())) return;
event.setCancelled(true);
this.getAppliance().openInventories.get(player).handleClick(
event.getCurrentItem(),
event.getSlot(),
event.getClick()
);
}
}

View File

@ -0,0 +1,16 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.packSelect.listeners;
import eu.mhsl.craftattack.core.appliance.ApplianceListener;
import eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.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(this.getAppliance().isNotPackInventory(player, event.getInventory())) return;
this.getAppliance().openInventories.remove(player);
}
}

View File

@ -0,0 +1,18 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.packSelect.listeners;
import eu.mhsl.craftattack.core.Main;
import eu.mhsl.craftattack.core.appliance.ApplianceListener;
import eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.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(),
() -> this.getAppliance().setPack(event.getPlayer(), this.getAppliance().getPackConfigurationForPlayer(event.getPlayer()))
);
}
}

View File

@ -0,0 +1,32 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.playtime;
import eu.mhsl.craftattack.core.appliance.Appliance;
import eu.mhsl.craftattack.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.core.util.text.DataSizeConverter;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.util.Ticks;
import org.bukkit.OfflinePlayer;
import org.bukkit.Statistic;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Objects;
public class Playtime extends Appliance {
public Component getFormattedPlaytime(OfflinePlayer player) {
int playtimeInTicks = player.getStatistic(Statistic.PLAY_ONE_MINUTE);
String playtime = DataSizeConverter.formatSecondsToHumanReadable(playtimeInTicks / Ticks.TICKS_PER_SECOND);
return Component.text()
.append(Component.text("Der Spieler ", NamedTextColor.GRAY))
.append(Component.text(Objects.requireNonNull(player.getName())))
.append(Component.text(" hat eine Spielzeit von ", NamedTextColor.GRAY))
.append(Component.text(playtime, NamedTextColor.GOLD))
.build();
}
@Override
protected @NotNull List<ApplianceCommand<?>> commands() {
return List.of(new PlaytimeCommand());
}
}

View File

@ -0,0 +1,31 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.playtime;
import eu.mhsl.craftattack.core.Main;
import eu.mhsl.craftattack.core.appliance.ApplianceCommand;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
class PlaytimeCommand extends ApplianceCommand.PlayerChecked<Playtime> {
public PlaytimeCommand() {
super("playtime");
}
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception {
String playerName = args.length == 1 ? args[0] : this.getPlayer().getName();
Bukkit.getScheduler().runTaskAsynchronously(Main.instance(), () -> {
OfflinePlayer player = Bukkit.getOfflinePlayer(playerName);
if(!player.hasPlayedBefore()) {
sender.sendMessage(Component.text("Der Spieler existiert nicht!", NamedTextColor.RED));
return;
}
sender.sendMessage(this.getAppliance().getFormattedPlaytime(player));
});
}
}

View File

@ -0,0 +1,177 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.report;
import eu.mhsl.craftattack.core.Main;
import eu.mhsl.craftattack.core.api.client.ReqResp;
import eu.mhsl.craftattack.core.api.client.repositories.ReportRepository;
import eu.mhsl.craftattack.core.appliance.Appliance;
import eu.mhsl.craftattack.core.appliance.ApplianceCommand;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentBuilder;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Optional;
public class Report extends Appliance {
public static Component helpText() {
return Component.text()
.appendNewline()
.append(Component.text(" Um einen Spieler zu melden, verwende ", NamedTextColor.GRAY)).appendNewline()
.append(Component.text("/report", NamedTextColor.GOLD)).appendNewline()
.append(Component.text("oder", NamedTextColor.GRAY)).appendNewline()
.append(Component.text("/report <spieler> [grund]", NamedTextColor.GOLD)).appendNewline()
.build();
}
public Report() {
super("report");
}
public void reportToUnknown(@NotNull Player issuer) {
ReportRepository.ReportCreationInfo request = new ReportRepository.ReportCreationInfo(issuer.getUniqueId(), null, "");
Bukkit.getScheduler().runTaskAsynchronously(
Main.instance(),
() -> this.createReport(issuer, request)
);
}
public void reportToKnown(@NotNull Player issuer, @NotNull String targetUsername, @Nullable String reason) {
OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(targetUsername);
if(issuer.getUniqueId().equals(offlinePlayer.getUniqueId())) {
issuer.sendMessage(Component.text("Du kannst dich nicht selbst reporten.", NamedTextColor.RED));
return;
}
ReportRepository.ReportCreationInfo request = new ReportRepository.ReportCreationInfo(
issuer.getUniqueId(),
offlinePlayer.getUniqueId(),
Optional.ofNullable(reason).orElse("")
);
Bukkit.getScheduler().runTaskAsynchronously(
Main.instance(),
() -> this.createReport(issuer, request)
);
}
private void createReport(Player issuer, ReportRepository.ReportCreationInfo reportRequest) {
ReqResp<ReportRepository.ReportUrl> createdReport = this.queryRepository(ReportRepository.class)
.createReport(reportRequest);
switch(createdReport.status()) {
case 201:
issuer.sendMessage(
Component.text()
.append(Component.text("\\/".repeat(20), NamedTextColor.DARK_GRAY))
.appendNewline()
.append(Component.text("⚠ Der Report muss über den folgenden Link fertiggestellt werden!", NamedTextColor.GOLD))
.appendNewline()
.appendNewline()
.append(
Component
.text(createdReport.data().url(), NamedTextColor.GRAY) // URL mit Weltkugel-Emoji
.clickEvent(ClickEvent.clickEvent(ClickEvent.Action.OPEN_URL, createdReport.data().url()))
)
.appendNewline()
.appendNewline()
.append(Component.text("Ohne das Fertigstellen des Reports wird dieser nicht bearbeitet!", NamedTextColor.DARK_RED))
.appendNewline()
.append(Component.text("/\\".repeat(20), NamedTextColor.DARK_GRAY))
);
break;
case 400:
issuer.sendMessage(
Component.text()
.append(Component.text("Der angegebene Nutzer ist in unserem System nicht bekannt.", NamedTextColor.RED))
.appendNewline()
.append(Component.text("Bist du sicher, dass du den Namen richtig geschrieben hast?", NamedTextColor.RED))
.appendNewline()
.append(Component.text("Du kannst dich alternativ jederzeit bei einem Admin melden.", NamedTextColor.GRAY))
);
break;
case 401:
default:
Main.logger().warning("Failed to request Report: " + createdReport.status());
issuer.sendMessage(
Component.text()
.append(Component.text("Interner Serverfehler beim anlegen des Reports.", NamedTextColor.RED))
.appendNewline()
.append(Component.text("Bitte melde dich bei einem Admin!", NamedTextColor.RED))
);
break;
}
}
public void queryReports(Player issuer) {
ReqResp<ReportRepository.PlayerReports> userReports = this.queryRepository(ReportRepository.class)
.queryReports(issuer.getUniqueId());
if(userReports.status() != 200) {
Main.logger().warning("Failed to request Reports: " + userReports.status());
issuer.sendMessage(
Component.text()
.append(Component.text("Interner Serverfehler beim abfragen der Reports.", NamedTextColor.RED))
.appendNewline()
.append(Component.text("Bitte melde dich bei einem Admin!", NamedTextColor.RED))
);
return;
}
List<ReportRepository.PlayerReports.Report> reports = userReports
.data()
.from_self()
.stream()
.filter(report -> !report.draft())
.toList()
.reversed();
if(reports.isEmpty()) {
issuer.sendMessage(
Component.text()
.append(Component.text("Du hast noch niemanden reportet.", NamedTextColor.RED))
.appendNewline()
.append(Component.text("Um jemanden zu melden, nutze /report", NamedTextColor.GRAY))
);
return;
}
ComponentBuilder<TextComponent, TextComponent.Builder> component = Component.text()
.append(Component.newline())
.append(Component.text("Von dir erstellte Reports: ", NamedTextColor.GOLD))
.appendNewline();
reports.forEach(report -> {
component
.append(Component.text(" - ", NamedTextColor.WHITE))
.append(
report.reported() != null
? Component.text(report.reported().username(), NamedTextColor.WHITE)
: Component.text("Unbekannt", NamedTextColor.YELLOW)
)
.append(Component.text(String.format(": %s", report.subject()), NamedTextColor.GRAY))
.clickEvent(ClickEvent.openUrl(report.url()))
.hoverEvent(HoverEvent.showText(Component.text("Klicke, um den Report einzusehen.", NamedTextColor.GOLD)));
component.appendNewline();
});
issuer.sendMessage(component.build());
}
@Override
@NotNull
protected List<ApplianceCommand<?>> commands() {
return List.of(
new ReportCommand(),
new ReportsCommand()
);
}
}

View File

@ -0,0 +1,65 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.report;
import eu.mhsl.craftattack.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.core.util.text.ComponentUtil;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
class ReportCommand extends ApplianceCommand.PlayerChecked<Report> {
public ReportCommand() {
super("report");
}
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
sender.sendMessage(ComponentUtil.pleaseWait());
if(args.length == 0) {
this.getAppliance().reportToUnknown(this.getPlayer());
}
if(args.length == 1) {
this.getAppliance().reportToKnown(this.getPlayer(), args[0], null);
}
if(args.length > 1) {
this.getAppliance().reportToKnown(this.getPlayer(), args[0], Arrays.stream(args).skip(1).collect(Collectors.joining(" ")));
}
}
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
List<String> response = new ArrayList<>();
if(args.length == 1) {
response = Stream.concat(
Bukkit.getOnlinePlayers().stream().map(Player::getName),
Arrays.stream(Bukkit.getOfflinePlayers()).map(OfflinePlayer::getName)
).toList();
}
if(args.length == 2) {
response = List.of(
"Griefing",
"Diebstahl",
"Beleidigung",
"Hacking",
"Andere Regelverstöße",
" "
);
}
return super.tabCompleteReducer(response, args);
}
}

View File

@ -0,0 +1,24 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.report;
import eu.mhsl.craftattack.core.Main;
import eu.mhsl.craftattack.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.core.util.text.ComponentUtil;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
class ReportsCommand extends ApplianceCommand.PlayerChecked<Report> {
public ReportsCommand() {
super("reports");
}
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
sender.sendMessage(ComponentUtil.pleaseWait());
Bukkit.getScheduler().runTaskAsynchronously(
Main.instance(),
() -> this.getAppliance().queryReports(this.getPlayer())
);
}
}

View File

@ -0,0 +1,19 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.sleepTag;
import eu.mhsl.craftattack.core.appliance.ApplianceListener;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerBedEnterEvent;
import org.bukkit.event.player.PlayerBedLeaveEvent;
class SleepStateChangeListener extends ApplianceListener<SleepTag> {
@EventHandler
public void onBedEnter(PlayerBedEnterEvent event) {
if(!event.getBedEnterResult().equals(PlayerBedEnterEvent.BedEnterResult.OK)) return;
this.getAppliance().updateSleeping(event.getPlayer(), true);
}
@EventHandler
public void onBedLeave(PlayerBedLeaveEvent event) {
this.getAppliance().updateSleeping(event.getPlayer(), false);
}
}

View File

@ -0,0 +1,69 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.sleepTag;
import eu.mhsl.craftattack.core.Main;
import eu.mhsl.craftattack.core.appliance.Appliance;
import eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.displayName.DisplayName;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.util.Ticks;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class SleepTag extends Appliance implements DisplayName.Prefixed {
private final Set<Player> sleepingPlayers = new HashSet<>();
@Override
public void onEnable() {
Bukkit.getScheduler().runTaskTimerAsynchronously(
Main.instance(),
this::cleanup,
Ticks.TICKS_PER_SECOND * 60,
Ticks.TICKS_PER_SECOND * 60
);
}
public void updateSleeping(Player player, boolean isSleeping) {
if(isSleeping) {
this.sleepingPlayers.add(player);
} else {
this.sleepingPlayers.remove(player);
}
this.updateDisplayName(player);
}
private void updateDisplayName(Player player) {
Main.instance().getAppliance(DisplayName.class).update(player);
}
private void cleanup() {
List<Player> invalidEntries = this.sleepingPlayers.stream()
.filter(player -> !player.isConnected())
.filter(player -> !player.isSleeping())
.toList();
invalidEntries.forEach(this.sleepingPlayers::remove);
invalidEntries.forEach(this::updateDisplayName);
}
@Override
public @Nullable Component getNamePrefix(Player player) {
if(this.sleepingPlayers.contains(player))
return Component.text("[\uD83D\uDCA4]", NamedTextColor.GRAY)
.hoverEvent(HoverEvent.showText(Component.text("Der Spieler liegt in einem Bett")));
return null;
}
@Override
protected @NotNull List<Listener> listeners() {
return List.of(new SleepStateChangeListener());
}
}

View File

@ -0,0 +1,90 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.tablist;
import eu.mhsl.craftattack.core.Main;
import eu.mhsl.craftattack.core.appliance.Appliance;
import eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.report.Report;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
import eu.mhsl.craftattack.core.util.IteratorUtil;
import eu.mhsl.craftattack.core.util.statistics.NetworkMonitor;
import eu.mhsl.craftattack.core.util.text.ComponentUtil;
import eu.mhsl.craftattack.core.util.text.RainbowComponent;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.util.Ticks;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.jetbrains.annotations.NotNull;
import java.lang.management.ManagementFactory;
import java.lang.management.OperatingSystemMXBean;
import java.time.Duration;
import java.util.List;
public class Tablist extends Appliance {
private final RainbowComponent serverName = new RainbowComponent(" CraftAttack 7 ", 7, 3);
private NetworkMonitor networkMonitor;
private OperatingSystemMXBean systemMonitor;
public Tablist() {
super("tablist");
}
@Override
public void onEnable() {
Settings.instance().declareSetting(TechnicalTablistSetting.class);
int tabRefreshRate = 3;
this.networkMonitor = new NetworkMonitor(this.localConfig().getString("interface"), Duration.ofSeconds(1));
this.systemMonitor = ManagementFactory.getOperatingSystemMXBean();
Bukkit.getScheduler().runTaskTimerAsynchronously(
Main.instance(),
() -> IteratorUtil.onlinePlayers(this::updateHeader),
tabRefreshRate * Ticks.TICKS_PER_SECOND,
tabRefreshRate * Ticks.TICKS_PER_SECOND
);
}
@Override
public void onDisable() {
this.networkMonitor.stop();
}
public void fullUpdate(Player player) {
this.updateHeader(player);
this.updateFooter(player);
}
private void updateHeader(Player player) {
boolean detailedInfo = this.queryAppliance(Settings.class).getSetting(player, Settings.Key.TechnicalTab, Boolean.class);
Component header = Component.newline()
.append(this.serverName.getRainbowState()).appendNewline()
.append(Component.text("mhsl.eu", NamedTextColor.GOLD)).appendNewline().appendNewline()
.append(ComponentUtil.getFormattedTickTimes(detailedInfo)).appendNewline();
if(detailedInfo) {
header = header
.appendNewline()
.append(ComponentUtil.getFormattedPing(player)).appendNewline()
.append(ComponentUtil.getFormattedNetworkStats(
this.networkMonitor.getTraffic(),
this.networkMonitor.getPackets()
)).appendNewline()
.append(ComponentUtil.getFormattedSystemStats(this.systemMonitor)).appendNewline();
}
player.sendPlayerListHeader(header);
}
private void updateFooter(Player player) {
player.sendPlayerListFooter(Report.helpText());
}
@Override
@NotNull
protected List<Listener> listeners() {
return List.of(new TablistListener());
}
}

View File

@ -0,0 +1,12 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.tablist;
import eu.mhsl.craftattack.core.appliance.ApplianceListener;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerJoinEvent;
class TablistListener extends ApplianceListener<Tablist> {
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
this.getAppliance().fullUpdate(event.getPlayer());
}
}

View File

@ -0,0 +1,38 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.tablist;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.CategorizedSetting;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.SettingCategory;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings;
import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.datatypes.BoolSetting;
import org.bukkit.Material;
public class TechnicalTablistSetting extends BoolSetting implements CategorizedSetting {
public TechnicalTablistSetting() {
super(Settings.Key.TechnicalTab);
}
@Override
protected String title() {
return "Technische Informationen";
}
@Override
protected String description() {
return "Zeige erweiterte Informationen und Statistiken in der Tabliste an";
}
@Override
protected Material icon() {
return Material.COMMAND_BLOCK_MINECART;
}
@Override
protected Boolean defaultValue() {
return false;
}
@Override
public SettingCategory category() {
return SettingCategory.Misc;
}
}

View File

@ -0,0 +1,17 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.worldmuseum;
import eu.mhsl.craftattack.core.appliance.ApplianceCommand;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
class MoveWorldMuseumVillagerCommand extends ApplianceCommand.PlayerChecked<WorldMuseum> {
public MoveWorldMuseumVillagerCommand() {
super("moveWorldMuseumVillager");
}
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
this.getAppliance().updateVillagerPosition(this.getPlayer().getLocation());
}
}

View File

@ -0,0 +1,62 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.worldmuseum;
import eu.mhsl.craftattack.core.Main;
import eu.mhsl.craftattack.core.appliance.Appliance;
import eu.mhsl.craftattack.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.core.util.entity.DisplayVillager;
import eu.mhsl.craftattack.core.util.listener.DismissInventoryOpenFromHolder;
import eu.mhsl.craftattack.core.util.listener.PlayerInteractAtEntityEventListener;
import eu.mhsl.craftattack.core.util.server.PluginMessage;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.entity.Villager;
import org.bukkit.event.Listener;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public class WorldMuseum extends Appliance {
public DisplayVillager.ConfigBound villager;
public WorldMuseum() {
super("worldMuseum");
}
@Override
public void onEnable() {
this.villager = new DisplayVillager.ConfigBound(
this.localConfig(),
villager -> {
villager.customName(Component.text("Museum der Welten").color(NamedTextColor.GOLD));
villager.setProfession(Villager.Profession.CARTOGRAPHER);
villager.setVillagerType(Villager.Type.SNOW);
}
);
}
public void updateVillagerPosition(Location location) {
this.villager.updateLocation(location);
}
public void handleVillagerInteraction(Player player) {
Main.logger().info("Sending" + player.getName() + " to WorldMuseum");
PluginMessage.connect(player, this.localConfig().getString("connect-server-name"));
}
@Override
@NotNull
protected List<ApplianceCommand<?>> commands() {
return List.of(new MoveWorldMuseumVillagerCommand());
}
@Override
@NotNull
protected List<Listener> listeners() {
return List.of(
new PlayerInteractAtEntityEventListener(this.villager.getUniqueId(), playerInteractAtEntityEvent -> this.handleVillagerInteraction(playerInteractAtEntityEvent.getPlayer())),
new DismissInventoryOpenFromHolder(this.villager.getUniqueId())
);
}
}

View File

@ -0,0 +1,103 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.yearRank;
import eu.mhsl.craftattack.core.Main;
import eu.mhsl.craftattack.core.appliance.Appliance;
import eu.mhsl.craftattack.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.displayName.DisplayName;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.*;
public class YearRank extends Appliance implements DisplayName.Prefixed {
record CraftAttackYear(String name) {
}
private final Map<UUID, List<CraftAttackYear>> rankMap = new HashMap<>();
@Override
public void onEnable() {
File folder = new File(Main.instance().getDataFolder(), "yearRank");
//noinspection ResultOfMethodCallIgnored
folder.mkdirs();
Optional<File[]> dataFolders = Optional.ofNullable(folder.listFiles());
if(dataFolders.isEmpty()) return;
List.of(dataFolders.get()).forEach(playerDataFolder -> {
Optional<File[]> datFiles = Optional.ofNullable(playerDataFolder.listFiles());
if(datFiles.isEmpty()) return;
CraftAttackYear craftAttackYear = new CraftAttackYear(playerDataFolder.getName());
Arrays.stream(datFiles.get())
.map(file -> file.getName().split("\\.")[0])
.distinct()
.map(UUID::fromString)
.peek(uuid -> this.rankMap.computeIfAbsent(uuid, p -> new ArrayList<>()))
.forEach(uuid -> this.rankMap.get(uuid).add(craftAttackYear));
});
}
@Override
public @Nullable Component getNamePrefix(Player player) {
if(!this.rankMap.containsKey(player.getUniqueId())) return null;
int yearCount = this.rankMap.get(player.getUniqueId()).size();
if(yearCount <= 3) return null;
return Component.text()
.append(Component.text("[\uD83C\uDF1F]", NamedTextColor.GOLD))
.hoverEvent(HoverEvent.showText(
Component.text(String.format("Langzeitspieler: %s ist bereits seit %d Jahren dabei!", player.getName(), yearCount))
))
.build();
}
public Component listYearRanks() {
TextComponent.Builder builder = Component.text();
builder.append(Component.text("Top 30 Spieler: ", NamedTextColor.GOLD));
this.rankMap.keySet().stream()
.map(uuid -> Map.entry(uuid, this.rankMap.get(uuid).size()))
.sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
.limit(30)
.forEach(entry -> builder
.appendNewline()
.append(Component.text(entry.getKey().toString(), NamedTextColor.GRAY)
.hoverEvent(HoverEvent.showText(Component.text(entry.getKey().toString())))
.clickEvent(ClickEvent.copyToClipboard(entry.getKey().toString())))
.append(Component.text(": "))
.append(Component.text(entry.getValue(), NamedTextColor.GOLD)));
builder
.appendNewline()
.appendNewline()
.append(Component.text("Übersischt:", NamedTextColor.GOLD));
this.rankMap.values().stream()
.mapMulti(Iterable::forEach)
.filter(o -> o instanceof CraftAttackYear)
.map(o -> (CraftAttackYear) o)
.distinct()
.forEach(craftAttackYear -> builder
.appendNewline()
.append(Component.text(craftAttackYear.name, NamedTextColor.GRAY))
.append(Component.text(": "))
.append(Component.text(this.rankMap.keySet().stream()
.filter(uuid -> this.rankMap.get(uuid).contains(craftAttackYear)).count())));
return builder.build();
}
@Override
protected @NotNull List<ApplianceCommand<?>> commands() {
return List.of(new YearRankCommand());
}
}

View File

@ -0,0 +1,17 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.metaGameplay.yearRank;
import eu.mhsl.craftattack.core.appliance.ApplianceCommand;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
class YearRankCommand extends ApplianceCommand<YearRank> {
public YearRankCommand() {
super("yearRank");
}
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception {
sender.sendMessage(this.getAppliance().listYearRanks());
}
}

View File

@ -0,0 +1,19 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.tooling.lightningFireControl;
import eu.mhsl.craftattack.core.appliance.Appliance;
import org.bukkit.block.Block;
import org.bukkit.event.Listener;
import org.jetbrains.annotations.NotNull;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class LightningFireControl extends Appliance {
Set<Block> frozenFireBlocks = new HashSet<>();
@Override
protected @NotNull List<Listener> listeners() {
return List.of(new LightningFireListener());
}
}

View File

@ -0,0 +1,62 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.tooling.lightningFireControl;
import eu.mhsl.craftattack.core.Main;
import eu.mhsl.craftattack.core.appliance.ApplianceListener;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.event.EventHandler;
import org.bukkit.event.block.BlockBurnEvent;
import org.bukkit.event.block.BlockIgniteEvent;
import org.bukkit.event.block.BlockSpreadEvent;
import org.bukkit.scheduler.BukkitRunnable;
import java.util.Random;
class LightningFireListener extends ApplianceListener<LightningFireControl> {
private final Random rnd = new Random();
@EventHandler
public void onBlockIgnite(BlockIgniteEvent event) {
if(!event.getCause().equals(BlockIgniteEvent.IgniteCause.LIGHTNING)) return;
Block block = event.getBlock();
this.getAppliance().frozenFireBlocks.add(block);
new BukkitRunnable() {
@Override
public void run() {
Runnable cancel = () -> {
LightningFireListener.this.getAppliance().frozenFireBlocks.remove(block);
this.cancel();
};
if (block.getType() != Material.FIRE) {
cancel.run();
return;
}
if (LightningFireListener.this.rnd.nextInt(100) < 30) {
block.setType(Material.AIR);
cancel.run();
}
}
}.runTaskTimer(Main.instance(), 20L, 20L);
}
@EventHandler
public void onFireSpread(BlockSpreadEvent event) {
Block source = event.getSource();
if(!source.getType().equals(Material.FIRE)) return;
if(source.equals(event.getBlock())) return;
if(!this.getAppliance().frozenFireBlocks.contains(source)) return;
event.setCancelled(true);
}
@EventHandler
public void onBlockBurned(BlockBurnEvent event) {
if(!this.getAppliance().frozenFireBlocks.contains(event.getIgnitingBlock())) return;
event.setCancelled(true);
}
}

View File

@ -0,0 +1,202 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.tooling.projectStart;
import eu.mhsl.craftattack.core.Main;
import eu.mhsl.craftattack.core.appliance.Appliance;
import eu.mhsl.craftattack.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.customAdvancements.Advancements;
import eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.customAdvancements.CustomAdvancements;
import eu.mhsl.craftattack.spawn.craftattack.appliances.tooling.projectStart.command.ProjectStartCancelCommand;
import eu.mhsl.craftattack.spawn.craftattack.appliances.tooling.projectStart.command.ProjectStartCommand;
import eu.mhsl.craftattack.spawn.craftattack.appliances.tooling.projectStart.command.ProjectStartResetCommand;
import eu.mhsl.craftattack.spawn.craftattack.appliances.tooling.projectStart.listener.NoAdvancementsListener;
import eu.mhsl.craftattack.spawn.craftattack.appliances.tooling.projectStart.listener.PlayerInvincibleListener;
import eu.mhsl.craftattack.core.config.Configuration;
import eu.mhsl.craftattack.core.util.IteratorUtil;
import eu.mhsl.craftattack.core.util.entity.PlayerUtils;
import eu.mhsl.craftattack.core.util.text.ComponentUtil;
import eu.mhsl.craftattack.core.util.text.Countdown;
import eu.mhsl.craftattack.core.util.world.BlockCycle;
import net.kyori.adventure.sound.Sound;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.*;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import static java.util.Map.entry;
import static org.bukkit.Sound.MUSIC_DISC_PRECIPICE;
public class ProjectStart extends Appliance {
private final int startMusicAt = 293;
private final World startWorld = Bukkit.getWorld("world");
private final Location glassLocation = new Location(this.startWorld, 0, 64, -300);
private final List<Location> netherFireLocations = List.of(
new Location(this.startWorld, 14, 71, -310)
);
private final Countdown countdown = new Countdown(
this.localConfig().getInt("countdown"),
this::format,
this::announce,
this::startProject
);
private final BlockCycle blockCycle = new BlockCycle(
this.glassLocation,
Material.RED_STAINED_GLASS,
List.of(
Material.RED_STAINED_GLASS,
Material.YELLOW_STAINED_GLASS,
Material.GREEN_STAINED_GLASS,
Material.BLUE_STAINED_GLASS
)
);
private final Map<GameRule<Boolean>, Boolean> gameRulesAfterStart = Map.ofEntries(
entry(GameRule.DO_DAYLIGHT_CYCLE, true),
entry(GameRule.DO_INSOMNIA, true),
entry(GameRule.ANNOUNCE_ADVANCEMENTS, true),
entry(GameRule.DISABLE_RAIDS, false),
entry(GameRule.DO_FIRE_TICK, true),
entry(GameRule.DO_ENTITY_DROPS, true),
entry(GameRule.DO_PATROL_SPAWNING, true),
entry(GameRule.DO_TRADER_SPAWNING, true),
entry(GameRule.DO_WEATHER_CYCLE, true),
entry(GameRule.FALL_DAMAGE, true),
entry(GameRule.FIRE_DAMAGE, true)
);
public ProjectStart() {
super("countdown");
this.countdown.addCustomAnnouncement(
new Countdown.CustomAnnouncements(
counter -> counter == this.startMusicAt,
counter -> this.glassLocation
.getWorld()
.playSound(this.glassLocation, MUSIC_DISC_PRECIPICE, SoundCategory.RECORDS, 500f, 1f)
)
);
}
private Component format(Countdown.AnnouncementData data) {
return Component.text()
.append(ComponentUtil.createRainbowText("CraftAttack", 10))
.append(Component.text(" startet in ", NamedTextColor.GOLD))
.append(Component.text(data.count(), NamedTextColor.AQUA))
.append(Component.text(" " + data.unit() + "!", NamedTextColor.GOLD))
.build();
}
private void announce(Component message) {
this.blockCycle.next();
IteratorUtil.onlinePlayers(player -> player.sendMessage(message));
}
private void resetAdvancements() {
Bukkit.getServer().advancementIterator().forEachRemaining(
advancement -> Bukkit.getOnlinePlayers().forEach(
player -> player.getAdvancementProgress(advancement).getAwardedCriteria().forEach(
criteria -> player.getAdvancementProgress(advancement).revokeCriteria(criteria)
)
)
);
}
public void startCountdown() {
if(!this.isEnabled()) return;
this.countdown.start();
}
public void cancelCountdown() {
this.countdown.cancel();
this.restoreBeforeStart();
}
public void startProject() {
this.setEnabled(false);
IteratorUtil.worlds(World::getWorldBorder, worldBorder -> worldBorder.setSize(worldBorder.getMaxSize()));
IteratorUtil.worlds(world -> IteratorUtil.setGameRules(this.gameRulesAfterStart, false));
IteratorUtil.worlds(world -> world.setFullTime(0));
this.netherFireLocations.forEach(location -> Objects.requireNonNull(this.startWorld).getBlockAt(location).setType(Material.FIRE));
Bukkit.getOnlinePlayers().forEach(player -> {
player.setFoodLevel(20);
player.setHealth(20);
player.getInventory().clear();
player.setGameMode(GameMode.SURVIVAL);
player.setExp(0);
player.setLevel(0);
player.playSound(Sound.sound(org.bukkit.Sound.ITEM_GOAT_HORN_SOUND_5, Sound.Source.MASTER, 500f, 1f));
player.sendMessage(Component.text("Viel Spaß bei CraftAttack!", NamedTextColor.GREEN));
player.setStatistic(Statistic.TIME_SINCE_REST, 0);
PlayerUtils.resetStatistics(player);
});
this.resetAdvancements();
Bukkit.getOnlinePlayers().forEach(
player -> Main.instance().getAppliance(CustomAdvancements.class).grantAdvancement(Advancements.start, player.getUniqueId())
);
this.blockCycle.reset();
}
public void restoreBeforeStart() {
this.setEnabled(true);
IteratorUtil.onlinePlayers(Player::stopAllSounds);
IteratorUtil.worlds(World::getWorldBorder, worldBorder -> {
worldBorder.setSize(this.localConfig().getLong("worldborder-before"));
worldBorder.setWarningDistance(0);
worldBorder.setDamageAmount(0);
});
IteratorUtil.worlds(world -> world, world -> IteratorUtil.setGameRules(this.gameRulesAfterStart, true));
this.resetAdvancements();
this.blockCycle.reset();
}
public boolean isEnabled() {
return this.localConfig().getBoolean("enabled");
}
public void setEnabled(boolean enabled) {
this.localConfig().set("enabled", enabled);
Configuration.saveChanges();
}
public Countdown getCountdown() {
return this.countdown;
}
@Override
@NotNull
protected List<Listener> listeners() {
return List.of(
new PlayerInvincibleListener(),
new NoAdvancementsListener()
);
}
@Override
@NotNull
protected List<ApplianceCommand<?>> commands() {
return List.of(
new ProjectStartCommand(),
new ProjectStartCancelCommand(),
new ProjectStartResetCommand()
);
}
}

View File

@ -0,0 +1,25 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.tooling.projectStart.command;
import eu.mhsl.craftattack.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.spawn.craftattack.appliances.tooling.projectStart.ProjectStart;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
public class ProjectStartCancelCommand extends ApplianceCommand<ProjectStart> {
public ProjectStartCancelCommand() {
super("projectStartCancel");
}
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if(this.getAppliance().getCountdown().isRunning()) {
this.getAppliance().cancelCountdown();
sender.sendMessage(Component.text("Countdown cancelled successfully!").color(NamedTextColor.GREEN));
} else {
sender.sendMessage(Component.text("Countdown is not running!").color(NamedTextColor.RED));
}
}
}

View File

@ -0,0 +1,30 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.tooling.projectStart.command;
import eu.mhsl.craftattack.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.spawn.craftattack.appliances.tooling.projectStart.ProjectStart;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
public class ProjectStartCommand extends ApplianceCommand<ProjectStart> {
public ProjectStartCommand() {
super("projectStart");
}
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if(!this.getAppliance().isEnabled()) {
sender.sendMessage(Component.text("Countdown not enabled or executed once before!").color(NamedTextColor.RED));
return;
}
if(this.getAppliance().getCountdown().isRunning()) {
sender.sendMessage(Component.text("Countdown already running!").color(NamedTextColor.RED));
return;
}
this.getAppliance().startCountdown();
}
}

View File

@ -0,0 +1,18 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.tooling.projectStart.command;
import eu.mhsl.craftattack.core.appliance.ApplianceCommand;
import eu.mhsl.craftattack.spawn.craftattack.appliances.tooling.projectStart.ProjectStart;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
public class ProjectStartResetCommand extends ApplianceCommand<ProjectStart> {
public ProjectStartResetCommand() {
super("projectStartReset");
}
@Override
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
this.getAppliance().restoreBeforeStart();
}
}

View File

@ -0,0 +1,22 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.tooling.projectStart.listener;
import eu.mhsl.craftattack.core.appliance.ApplianceListener;
import eu.mhsl.craftattack.spawn.craftattack.appliances.tooling.projectStart.ProjectStart;
import org.bukkit.advancement.Advancement;
import org.bukkit.advancement.AdvancementProgress;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerAdvancementDoneEvent;
public class NoAdvancementsListener extends ApplianceListener<ProjectStart> {
@EventHandler
public void onAdvancement(PlayerAdvancementDoneEvent event) {
if(!this.getAppliance().isEnabled()) return;
event.message(null);
Advancement advancement = event.getAdvancement();
AdvancementProgress progress = event.getPlayer().getAdvancementProgress(advancement);
for(String criteria : progress.getAwardedCriteria()) {
progress.revokeCriteria(criteria);
}
}
}

View File

@ -0,0 +1,27 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.tooling.projectStart.listener;
import eu.mhsl.craftattack.core.appliance.ApplianceListener;
import eu.mhsl.craftattack.spawn.craftattack.appliances.tooling.projectStart.ProjectStart;
import io.papermc.paper.event.player.PrePlayerAttackEntityEvent;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.FoodLevelChangeEvent;
public class PlayerInvincibleListener extends ApplianceListener<ProjectStart> {
@EventHandler
public void onDamage(EntityDamageEvent event) {
if(!(event.getEntity() instanceof Player)) return;
if(this.getAppliance().isEnabled()) event.setCancelled(true);
}
@EventHandler
public void onHunger(FoodLevelChangeEvent event) {
if(this.getAppliance().isEnabled()) event.setCancelled(true);
}
@EventHandler
public void onHit(PrePlayerAttackEntityEvent event) {
if(this.getAppliance().isEnabled()) event.setCancelled(true);
}
}

View File

@ -0,0 +1,33 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.tooling.whitelist;
import eu.mhsl.craftattack.core.appliance.ApplianceListener;
import eu.mhsl.craftattack.core.util.text.DisconnectInfo;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.AsyncPlayerPreLoginEvent;
import org.bukkit.event.player.PlayerLoginEvent;
class PlayerJoinListener extends ApplianceListener<Whitelist> {
@EventHandler
public void preLoginEvent(AsyncPlayerPreLoginEvent event) {
try {
this.getAppliance().integrityCheck(event.getUniqueId(), event.getName());
} catch(DisconnectInfo.Throwable e) {
event.disallow(
AsyncPlayerPreLoginEvent.Result.KICK_WHITELIST,
e.getDisconnectScreen().getComponent()
);
}
}
@EventHandler
public void joinEvent(PlayerLoginEvent event) {
try {
this.getAppliance().lateIntegrityCheck(event.getPlayer());
} catch(DisconnectInfo.Throwable e) {
event.disallow(
PlayerLoginEvent.Result.KICK_WHITELIST,
e.getDisconnectScreen().getComponent()
);
}
}
}

View File

@ -0,0 +1,151 @@
package eu.mhsl.craftattack.spawn.craftattack.appliances.tooling.whitelist;
import eu.mhsl.craftattack.core.Main;
import eu.mhsl.craftattack.core.api.client.ReqResp;
import eu.mhsl.craftattack.core.api.client.repositories.WhitelistRepository;
import eu.mhsl.craftattack.core.api.server.HttpServer;
import eu.mhsl.craftattack.core.appliance.Appliance;
import eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.outlawed.Outlawed;
import eu.mhsl.craftattack.core.util.api.HttpStatus;
import eu.mhsl.craftattack.core.util.server.Floodgate;
import eu.mhsl.craftattack.core.util.text.DisconnectInfo;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
import java.util.logging.Level;
public class Whitelist extends Appliance {
private final HashMap<UUID, WhitelistRepository.UserData> userData = new HashMap<>();
public Whitelist() {
super("whitelist");
}
public void fullIntegrityCheck(Player player) throws DisconnectInfo.Throwable {
this.integrityCheck(player.getUniqueId(), player.getName());
this.lateIntegrityCheck(player);
}
public void lateIntegrityCheck(Player player) throws DisconnectInfo.Throwable {
@Nullable WhitelistRepository.UserData user = this.userData.get(player.getUniqueId());
if(user == null) {
throw new DisconnectInfo.Throwable(
"Nutzerdaten nicht geladen",
"Deine Nutzerdaten sind noch nicht bereit!",
"warte einige Sekunden und versuche es erneut. Falls es weiterhin nicht funktioniert kontaktiere einen Admin!",
player.getUniqueId()
);
}
this.queryAppliance(Outlawed.class).updateForcedStatus(player, this.timestampRelevant(user.outlawed_until()));
String purePlayerName = Floodgate.isBedrock(player)
? Floodgate.getBedrockPlayer(player).getUsername()
: player.getName();
if(!user.username().trim().equalsIgnoreCase(purePlayerName))
throw new DisconnectInfo.Throwable(
"Nutzername geändert",
String.format("Der Name '%s' stimmt nicht mit '%s' überein.", user.username(), player.getName()),
"Bitte kontaktiere einen Admin, um Deine Anmeldedaten zu aktualisieren!",
player.getUniqueId()
);
}
public void integrityCheck(UUID uuid, String name) throws DisconnectInfo.Throwable {
try {
Main.instance().getLogger().info(String.format("Running integrityCheck for %s", name));
boolean overrideCheck = this.localConfig().getBoolean("overrideIntegrityCheck", false);
WhitelistRepository.UserData user = overrideCheck
? new WhitelistRepository.UserData(uuid, name, "", "", 0L, 0L)
: this.fetchUserData(uuid);
this.userData.put(uuid, user);
Main.logger().info(String.format("got userdata %s", user.toString()));
if(this.timestampRelevant(user.banned_until())) {
Instant bannedDate = new Date(user.banned_until() * 1000L)
.toInstant()
.plus(1, ChronoUnit.HOURS);
DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("dd.MM.yyyy").withZone(ZoneOffset.UTC);
DateTimeFormatter timeFormat = DateTimeFormatter.ofPattern("HH:mm").withZone(ZoneOffset.UTC);
throw new DisconnectInfo.Throwable(
"Du wurdest vom Server gebannt.",
String.format("Dein Bann läuft am %s um %s ab!", dateFormat.format(bannedDate), timeFormat.format(bannedDate)),
"Wende dich an einen Admin für weitere Informationen.",
uuid
);
}
} catch(DisconnectInfo.Throwable e) {
throw e;
} catch(Exception e) {
Main.instance().getLogger().log(Level.SEVERE, e, e::getMessage);
throw new DisconnectInfo.Throwable(
"Interner Serverfehler",
"Deine Anmeldedaten konnten nicht abgerufen/ überprüft werden.",
"Versuche es später erneut oder kontaktiere einen Admin!",
uuid
);
}
}
private boolean timestampRelevant(Long timestamp) {
if(timestamp == null) return false;
return timestamp > System.currentTimeMillis() / 1000L;
}
private WhitelistRepository.UserData fetchUserData(UUID uuid) throws DisconnectInfo.Throwable {
ReqResp<WhitelistRepository.UserData> response = this.queryRepository(WhitelistRepository.class).getUserData(uuid);
if(response.status() == HttpStatus.NOT_FOUND)
throw new DisconnectInfo.Throwable(
"Nicht angemeldet",
"Du bist derzeit nicht als Teilnehmer des CraftAttack-Projektes registriert!",
"Melde Dich bei einem Admin für eine nachträgliche Anmeldung.",
uuid
);
if(response.status() != HttpStatus.OK)
throw new IllegalStateException(String.format("Http Reponse %d", response.status()));
return response.data();
}
@Override
public void httpApi(HttpServer.ApiBuilder apiBuilder) {
record User(UUID user) {
}
apiBuilder.post("update", User.class, (user, request) -> {
Main.instance().getLogger().info(String.format("API Triggered Profile update for %s", user.user));
Player player = Bukkit.getPlayer(user.user);
if(player != null) {
try {
this.fullIntegrityCheck(player);
} catch(DisconnectInfo.Throwable e) {
e.getDisconnectScreen().applyKick(player);
}
}
return HttpServer.nothing;
});
}
@Override
@NotNull
protected List<Listener> listeners() {
return List.of(
new PlayerJoinListener()
);
}
}