v1.2.4: Remove roguelike perks, update bStats to 3.2.1, add new game modes and lpschem system

- Removed roguelike perk system (double jump, magnet, shield, second chance)
- Updated bStats dependency to 3.2.1 with proper relocation
- Changed bStats plugin ID to 29754
- Added new game modes: Speedrun, Gravity Shift, Hardcore
- Implemented .lpschem format with GZIP + JSON compression
- Added JumpValidator for physics-based jump validation
- Added JumpType system (neo-jump, head-hitter, fence, trapdoor, ladder)
- Implemented Ghost system for recording/playback top players
- Added loparkour-vilib as local dependency
- Updated config with new mode settings and removed roguelike section
- Cleaned up TODO.md roadmap
This commit is contained in:
loki 2026-02-25 00:55:24 +01:00
parent aecb03ccf8
commit 8aeb547897
68 changed files with 6186 additions and 163 deletions

View file

@ -0,0 +1,29 @@
plugins {
id 'java-library'
}
group = 'dev.loki.loparkour'
version = '2.0.1-loparkour'
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
repositories {
mavenCentral()
maven { url = 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' }
maven { url = 'https://jitpack.io' }
maven { url = 'https://oss.sonatype.org/content/groups/public' }
maven { url = 'https://repo.codemc.io/repository/maven-snapshots/' }
}
dependencies {
compileOnly 'org.spigotmc:spigot-api:1.20.4-R0.1-SNAPSHOT'
compileOnly 'org.jetbrains:annotations:24.1.0'
api 'fr.mrmicky:fastboard:2.1.2'
api 'org.bstats:bstats-bukkit:3.2.1'
api 'net.kyori:adventure-text-minimessage:4.16.0'
api 'net.kyori:adventure-text-serializer-legacy:4.16.0'
}

View file

@ -0,0 +1,90 @@
package dev.efnilite.vilib;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import dev.efnilite.vilib.command.ViCommand;
import dev.efnilite.vilib.util.Version;
import org.bukkit.Bukkit;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.plugin.java.JavaPlugin;
/**
* Main class which plugins may inherit to reduce the amount of setup required.
* Classes inheriting this may want to create static methods to inherit the logging and ViPlugin vars.
* Made by Efnilite, 2021-2023
*
* @since 1.0.0
*/
public abstract class ViPlugin extends JavaPlugin {
protected static Gson gson;
protected static Version version;
/**
* @return a Gson instance which has already been set up.
*/
public static Gson getGson() {
return gson;
}
/**
* @return The version.
*/
public static Version getVersion() {
return version;
}
@Override
public void onEnable() {
version = Version.getVersion();
gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().disableHtmlEscaping().create();
enable();
}
@Override
public void onDisable() {
disable();
HandlerList.unregisterAll(this);
Bukkit.getScheduler().cancelTasks(this);
}
/**
* What happens on enable of the plugin inheriting this library.
*/
public abstract void enable();
/**
* What happens on disable of the plugin inheriting this library.
* Disabling will automatically cancel all active tasks and unregister all EventWatchers.
*/
public abstract void disable();
/**
* Register a command to this plugin.
*
* @param name The name of the command in plugin.yml
* @param command The command class
*/
public void registerCommand(String name, ViCommand command) {
var cmd = getCommand(name);
if (cmd == null) return;
cmd.setExecutor(command);
cmd.setTabCompleter(command);
}
/**
* Registers a Listener, with this plugin as its owner.
*
* @param listener The listener to register.
* @see dev.efnilite.vilib.event.EventWatcher
*/
public void registerListener(Listener listener) {
getServer().getPluginManager().registerEvents(listener, this);
}
}

View file

@ -0,0 +1,118 @@
package dev.efnilite.vilib.command;
import org.bukkit.command.*;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.*;
/**
* Class to wrap commands, which makes it a lot easier to produce them.
*
* @author Efnilite
*/
public abstract class ViCommand implements CommandExecutor, TabCompleter {
/**
* UUID-based cooldown system
*/
private final Map<UUID, List<CommandCooldown>> cooldowns = new HashMap<>();
/**
* Execute a command
*/
public abstract boolean execute(CommandSender sender, String[] args);
/**
* Get what should be suggested
*/
public abstract List<String> tabComplete(CommandSender sender, String[] args);
/**
* Checks the cooldown
*
* @param sender The CommandSender which may have a cooldown
* @param arg The argument to which this cooldown applies
* @param cooldownMs The cooldown in ms
* @return false if the cooldown is not over yet, true if it has been.
*/
protected boolean cooldown(CommandSender sender, String arg, long cooldownMs) {
if (sender instanceof ConsoleCommandSender) { // ignore console (has no UUID)
return true;
}
Player player = (Player) sender;
UUID uuid = player.getUniqueId();
CommandCooldown cooldown; // the current cooldown
List<CommandCooldown> playerCooldowns = cooldowns.get(uuid);
if (playerCooldowns == null) {
playerCooldowns = new ArrayList<>();
playerCooldowns.add(new CommandCooldown(System.currentTimeMillis(), arg));
cooldowns.put(uuid, playerCooldowns);
return true;
}
// get the appropriate commandcooldown class
cooldown = playerCooldowns.stream()
.filter(plCooldown -> plCooldown.arg().equals(arg))
.findFirst().orElse(null);
if (cooldown == null) { // cooldown doesnt exist yet
playerCooldowns.add(new CommandCooldown(System.currentTimeMillis(), arg));
cooldowns.put(uuid, playerCooldowns);
return true;
}
if (System.currentTimeMillis() - cooldown.lastExecuted() <= cooldownMs) {
return false;
}
playerCooldowns.remove(cooldown); // update cooldown
playerCooldowns.add(new CommandCooldown(System.currentTimeMillis(), arg));
cooldowns.put(uuid, playerCooldowns);
return true;
}
/**
* Gets completions in relation to what the user has already typed
*
* @param typed What the player has typed so far
* @param possible The possible completions
* @return the updated possible completions
*/
protected List<String> completions(String typed, List<String> possible) {
return possible.stream().filter(option -> option.toLowerCase().contains(typed)).toList();
}
/**
* Gets completions in relation to what the user has already typed
*
* @param typed What the player has typed so far
* @param possible The possible completions
* @return the updated possible completions
*/
protected List<String> completions(String typed, String... possible) {
return Arrays.stream(possible).filter(option -> option.toLowerCase().contains(typed)).toList();
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
return execute(sender, args);
}
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
return tabComplete(sender, args);
}
/**
* @param arg The argument
* @param lastExecuted When the command with arg was last executed
*/
private record CommandCooldown(long lastExecuted, String arg) {
}
}

View file

@ -0,0 +1,14 @@
package dev.efnilite.vilib.configupdater;
import org.bukkit.plugin.Plugin;
import java.io.File;
import java.util.List;
public class ConfigUpdater {
public static void update(Plugin plugin, String resourceName, File toUpdate, List<String> ignoredSections) throws Exception {
// Stub implementation - config updating disabled
// Original library not available, manual config updates required
}
}

View file

@ -0,0 +1,28 @@
package dev.efnilite.vilib.event;
import org.bukkit.Bukkit;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.plugin.Plugin;
/**
* Wrapper for Listener, adding useful methods.
*
* @author Efnilite
*/
public interface EventWatcher extends Listener {
/**
* Unregisters every listener in this class
*/
default void unregisterAll() {
HandlerList.unregisterAll(this);
}
/**
* Registers this listener
*/
default void register(Plugin plugin) {
Bukkit.getPluginManager().registerEvents(this, plugin);
}
}

View file

@ -0,0 +1,32 @@
package dev.efnilite.vilib.event;
import org.bukkit.Bukkit;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
/**
* A class for removing unnecessary methods that are the same across all Event classes,
* making the actual Event classes cleaner.
*
* @author Efnilite
*/
public abstract class EventWrapper extends Event {
private static final HandlerList handlerList = new HandlerList();
protected boolean cancelled;
public boolean call() {
Bukkit.getPluginManager().callEvent(this);
return cancelled;
}
public static HandlerList getHandlerList() {
return handlerList;
}
@Override
public @NotNull HandlerList getHandlers() {
return handlerList;
}
}

View file

@ -0,0 +1,10 @@
package dev.efnilite.vilib.fastboard;
import org.bukkit.entity.Player;
public class FastBoard extends fr.mrmicky.fastboard.FastBoard {
public FastBoard(Player player) {
super(player);
}
}

View file

@ -0,0 +1,337 @@
package dev.efnilite.vilib.inventory;
import dev.efnilite.vilib.event.EventWatcher;
import dev.efnilite.vilib.inventory.item.Item;
import dev.efnilite.vilib.inventory.item.MenuItem;
import dev.efnilite.vilib.util.Numbers;
import dev.efnilite.vilib.util.Strings;
import dev.efnilite.vilib.util.Task;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitRunnable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.stream.IntStream;
/**
* Class for Menu handling
*
* @author Efnilite
*/
@SuppressWarnings("unused")
public class Menu implements EventWatcher {
private final static Map<UUID, UUID> openMenus = new HashMap<>();
private final static List<Menu> disabledMenus = new ArrayList<>();
private static Plugin plugin;
public static void init(Plugin pl) {
Task.create(pl).repeat(5 * 20).execute(() -> {
if (openMenus.keySet().isEmpty()) {
for (Menu menu : disabledMenus) {
menu.unregisterAll();
}
disabledMenus.clear();
}
}).run();
plugin = pl;
}
protected final int rows;
protected final String title;
protected final Map<Integer, MenuItem> items = new HashMap<>();
protected final List<Integer> evenlyDistributedRows = new ArrayList<>();
protected boolean deactivated = false;
protected UUID inventoryId;
protected Player player;
protected Material filler = null;
public Menu(int rows, String name) {
if (rows < 1 || rows > 6) {
throw new IllegalArgumentException("Rows must be above 1 and below 6");
}
this.rows = rows;
this.title = Strings.colour(name);
this.inventoryId = UUID.randomUUID();
}
/**
* Returns a list of slot numbers that evenly distribute the amount of items in a row.
*
* @param amountInRow The amount of items in the row.
* @return A list of slot numbers that ensure an equal distribution.
*/
public static List<Integer> getEvenlyDistributedSlots(int amountInRow) {
return switch (amountInRow) {
case 0 -> Collections.emptyList();
case 1 -> Collections.singletonList(4);
case 2 -> List.of(3, 5);
case 3 -> List.of(3, 4, 5);
case 4 -> List.of(2, 3, 5, 6);
case 5 -> List.of(2, 3, 4, 5, 6);
case 6 -> List.of(1, 2, 3, 5, 6, 7);
case 7 -> List.of(1, 2, 3, 4, 5, 6, 7);
case 8 -> List.of(0, 1, 2, 3, 5, 6, 7, 8);
default -> List.of(0, 1, 2, 3, 4, 5, 6, 7, 8);
};
}
/**
* Sets an item to a slot
*
* @param slot The slot
* @param item The item
* @return the instance of this class
*/
public Menu item(int slot, MenuItem item) {
if (slot > rows * 9 || slot < 0) {
throw new IllegalArgumentException("Slot %d is not in inventory".formatted(slot));
}
items.put(slot, item);
return this;
}
/**
* Sets a specific set of rows to be distributed evenly. The items will be distributed.
* Starts from 0 and goes up to 5.
*
* @param rows The rows
* @return the instance of this class
*/
public Menu distributeRowEvenly(int... rows) {
for (int row : rows) {
if (row < 0 || row > 5) {
throw new IllegalArgumentException("Rows must be above 1 and below 6");
}
evenlyDistributedRows.add(row);
}
return this;
}
/**
* Will distribute all rows evenly.
*
* @return the instance of this class
* @see #distributeRowEvenly(int...)
*/
public Menu distributeRowsEvenly() {
evenlyDistributedRows.addAll(Numbers.getFromZero(rows));
return this;
}
/**
* Fills the background with a specific item
*
* @param filler The background filler
* @return the instance of this class
*/
public Menu fillBackground(@NotNull Material filler) {
this.filler = filler;
return this;
}
/**
* Updates a specific item
*
* @param slots The slots which are to be updated
*/
public void updateItem(int... slots) {
Inventory inventory = player.getOpenInventory().getTopInventory();
if (inventory.getSize() % 9 != 0) {
throw new IllegalArgumentException("Invalid inventory type");
}
for (int slot : slots) {
inventory.setItem(slot, items.get(slot).build());
}
}
/**
* Updates all items in the inventory
*/
public void update() {
Inventory inventory = player.getOpenInventory().getTopInventory();
if (inventory.getSize() % 9 != 0) {
throw new IllegalArgumentException("Invalid inventory type");
}
inventory.clear();
items.forEach((slot, item) -> inventory.setItem(slot, item.build()));
}
/**
* Updates all items by calling {@link #update()} in the inventory periodically.
*
* @param tickInterval The amount of ticks to wait between calling {@link #update()}
*/
public void update(int tickInterval) {
if (tickInterval <= 0) {
throw new IllegalArgumentException("Tick interval must be above 0");
}
Task.create(plugin)
.repeat(tickInterval)
.execute(new BukkitRunnable() {
@Override
public void run() {
if (deactivated) {
cancel();
return;
}
update();
}
})
.run();
}
/**
* Opens the menu for the player. This distributes items on the same row automatically if these rows are assigned to automatically distribute.
*
* @param player The player to open it to
*/
public void open(Player player) {
this.player = player;
Inventory inventory = Bukkit.createInventory(null, rows * 9, title);
// Evenly distributed rows
for (int row : evenlyDistributedRows) {
int min = row * 9; // 0 * 9 = 0
int max = min + 8; // 0 + 8 = slot 8
Map<Integer, MenuItem> itemsInRow = new HashMap<>();
for (int slot : items.keySet()) { // get all items in the specified row
if (slot >= min && slot <= max) {
itemsInRow.put(slot, items.get(slot));
}
}
if (itemsInRow.keySet().isEmpty()) {
continue;
}
List<Integer> sortedSlots = itemsInRow.keySet().stream().sorted().toList(); // sort all slots
List<Integer> slots = getEvenlyDistributedSlots(sortedSlots.size()); // evenly distribute items
List<Integer> olds = new ArrayList<>();
List<Integer> news = new ArrayList<>();
for (int i = 0; i < slots.size(); i++) {
int newSlot = slots.get(i) + (9 * row); // gets the new slot
int oldSlot = sortedSlots.get(i); // the previous slot
MenuItem item = itemsInRow.get(oldSlot); // the item in the previous slot
news.add(newSlot);
olds.add(oldSlot);
items.put(newSlot, item); // put item in new slot
}
for (int oldSlot : olds) {
if (news.contains(oldSlot)) {
continue;
}
items.remove(oldSlot); // remove items from previous slot without deleting ones that are to-be moved
}
}
// Filler
if (filler != null) {
Item fillerItem = new Item(filler, "<red> "); // fill the background with the same material
// ignore already-set items
IntStream.range(0, rows * 9)
.filter(slot -> items.get(slot) == null)
.forEach(slot -> items.put(slot, fillerItem));
}
player.openInventory(inventory);
openMenus.put(player.getUniqueId(), inventoryId);
items.forEach((slot, item) -> inventory.setItem(slot, item.build()));
register(plugin);
}
@EventHandler
public void click(@NotNull InventoryClickEvent event) {
if (deactivated || event.getClickedInventory() != event.getView().getTopInventory()) {
return;
}
UUID id = openMenus.get(event.getWhoClicked().getUniqueId());
if (id != inventoryId) {
return;
}
MenuItem clickedItem = items.get(event.getSlot());
if (clickedItem == null) {
return;
}
event.setCancelled(!clickedItem.isMovable());
clickedItem.handleClick(this, event, event.getClick());
}
@EventHandler
public void close(InventoryCloseEvent event) {
if (deactivated) {
return;
}
UUID viewerId = event.getPlayer().getUniqueId();
UUID id = openMenus.get(viewerId);
if (id != inventoryId) {
return;
}
deactivated = true;
disabledMenus.add(this);
openMenus.remove(viewerId);
}
/**
* Returns the item in the respective slot.
*
* @param slot The slot
* @return the item in this slot. This may be null.
*/
public @Nullable MenuItem getItem(int slot) {
return items.get(slot);
}
/**
* Gets the slots and their respective items
*
* @return a Map with the slots and items.
*/
public Map<Integer, MenuItem> getItems() {
return items;
}
/**
* Gets the player
*
* @return the player
*/
public Player getPlayer() {
return player;
}
public String getTitle() {
return title;
}
}

View file

@ -0,0 +1,15 @@
package dev.efnilite.vilib.inventory;
import dev.efnilite.vilib.inventory.item.MenuItem;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryClickEvent;
/**
* Container class for clicking on menus
*/
public record MenuClickEvent(int slot, Menu menu, MenuItem item, InventoryClickEvent event) {
public Player getPlayer() {
return (Player) event.getWhoClicked();
}
}

View file

@ -0,0 +1,157 @@
package dev.efnilite.vilib.inventory;
import dev.efnilite.vilib.inventory.item.Item;
import dev.efnilite.vilib.inventory.item.MenuItem;
import dev.efnilite.vilib.util.Numbers;
import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* A menu which contains multiple pages
*
* @author Efnilite
*/
public class PagedMenu extends Menu {
private int total;
private int current;
private int nextPageSlot;
private MenuItem nextPageItem;
private int prevPageSlot;
private MenuItem prevPageItem;
private final List<Integer> displaySlots = new ArrayList<>();
private final List<MenuItem> totalToDisplay = new ArrayList<>();
private final Map<Integer, List<MenuItem>> assigned = new HashMap<>();
public PagedMenu(int rows, String name) {
super(rows, name);
}
/**
* Excludes a specific set of rows from being used to display items
*
* @param rows The rows which will be excluded. Starts from 0.
* @return the instance of this
*/
public PagedMenu displayRows(int... rows) {
int begin = Integer.MAX_VALUE;
int end = Integer.MIN_VALUE;
for (int row : rows) {
if (row < 0 || row > 5) {
throw new IllegalArgumentException("Row must be above 0 and below 6!");
}
int min = row * 9;
int max = min + 8;
begin = Numbers.min(begin, min);
end = Numbers.max(end, max);
}
displaySlots.addAll(Numbers.getFromTo(begin, end));
return this;
}
@Override
public void open(Player player) {
assignPages();
page(0);
super.open(player);
}
public void page(int delta) {
int newPage = current + delta;
if (newPage < 0 || newPage > total) {
return;
}
if (!assigned.containsKey(newPage) || assigned.get(newPage) == null) {
return;
}
List<MenuItem> values = new ArrayList<>(assigned.get(newPage));
Item missingFiller = new Item(filler, "<red> ");
items.remove(prevPageSlot);
items.remove(nextPageSlot);
if (newPage > 0) {
items.put(prevPageSlot, prevPageItem);
} else if (filler != null) {
items.put(prevPageSlot, missingFiller);
}
if (newPage < total - 1) {
items.put(nextPageSlot, nextPageItem);
} else if (filler != null) {
items.put(nextPageSlot, missingFiller);
}
for (int slot : displaySlots) {
items.remove(slot);
if (!values.isEmpty()) {
items.put(slot, values.get(0));
values.remove(0);
} else if (filler != null) {
items.put(slot, missingFiller);
}
}
if (delta != 0) {
update();
}
current = newPage;
}
private void assignPages() {
List<MenuItem> total = new ArrayList<>(totalToDisplay);
List<MenuItem> thisPage = new ArrayList<>();
int page = 0;
while (!total.isEmpty()) {
for (int slot = 0; slot < displaySlots.size(); slot++) {
if (total.isEmpty()) {
break;
}
thisPage.add(total.get(0));
total.remove(0);
}
this.assigned.put(page, new ArrayList<>(thisPage));
thisPage.clear();
page++;
}
this.current = 0;
this.total = assigned.keySet().size();
}
public PagedMenu addToDisplay(List<MenuItem> items) {
totalToDisplay.addAll(items);
return this;
}
public void setToDisplay(List<MenuItem> items) {
totalToDisplay.clear();
totalToDisplay.addAll(items);
}
public PagedMenu nextPage(int slot, MenuItem item) {
this.nextPageSlot = slot;
this.nextPageItem = item;
return this;
}
public PagedMenu prevPage(int slot, MenuItem item) {
this.prevPageSlot = slot;
this.prevPageItem = item;
return this;
}
public List<MenuItem> getTotalToDisplay() {
return totalToDisplay;
}
}

View file

@ -0,0 +1,147 @@
package dev.efnilite.vilib.inventory.item;
import com.google.common.annotations.Beta;
import dev.efnilite.vilib.inventory.Menu;
import dev.efnilite.vilib.inventory.MenuClickEvent;
import dev.efnilite.vilib.util.Colls;
import dev.efnilite.vilib.util.Task;
import org.bukkit.event.inventory.ClickType;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.inventory.InventoryView;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitRunnable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
/**
* Class for an item which automatically slides.
*
* @author Efnilite
*/
@Beta
public class AutoSliderItem extends MenuItem {
/**
* The initial value which will be displayed
*/
private int current;
private int cooldown;
private final int slot;
private final Plugin plugin;
private final Menu menu;
private final Map<Integer, Item> items = new HashMap<>();
private final Map<Integer, Consumer<MenuClickEvent>> clickFunctions = new HashMap<>();
/**
* The constructor.
*
* @param menu The menu that this item will be displayed in.
*/
public AutoSliderItem(int slot, Menu menu, Plugin plugin) {
this.slot = slot;
this.menu = menu;
this.plugin = plugin;
}
/**
* Sets the initial viewing index.
* If you set up 2 items, with index 0 and 1, you can specify which will be viewed first.
*
* @param initial The initial viewing index
* @return the instance of this class
*/
public AutoSliderItem initial(int initial) {
this.current = initial;
return this;
}
/**
* Sets the cooldown between sliding to the next item.
*
* @param ticks The amount of ticks until the transition occurs.
* @return the instance of this class
*/
public AutoSliderItem cooldown(int ticks) {
this.cooldown = ticks;
return this;
}
/**
* Adds an item to the possible options. This uses a Function.
* The Function will determine whether the item will update in the inventory.
* If this returns false, it will not update the item in the menu, but it will execute the code.
*
* @param value The value assigned to this item
* @param item The item
* @param onClick What happens on switch to this item. Returns true if it should update, false if not.
* @return the instance of this class
*/
public AutoSliderItem add(int value, Item item, Consumer<MenuClickEvent> onClick) {
items.put(value, item);
clickFunctions.put(value, onClick);
return this;
}
@Override
public void handleClick(Menu menu, InventoryClickEvent event, ClickType clickType) {
Consumer<MenuClickEvent> function = clickFunctions.get(current);
if (function == null) {
return;
}
function.accept(new MenuClickEvent(event.getSlot(), menu, this, event));
}
@Override
public ItemStack build() {
if (items.keySet().isEmpty()) {
throw new IllegalArgumentException("Items size is <0 or 0!");
}
Item init = items.get(current);
if (init == null) {
init = items.get(Colls.random(new ArrayList<>(items.keySet())));
}
if (items.size() > 1) { // loop through if there is more than 1 player
BukkitRunnable runnable = new BukkitRunnable() {
@Override
public void run() {
InventoryView view = menu.getPlayer().getOpenInventory();
if (view.getTitle().equals(menu.getTitle())) {
view.getTopInventory().setItem(slot, getNextItem().build());
} else {
cancel(); // prevent going on forever
}
}
};
Task task = Task.create(plugin).delay(cooldown).repeat(cooldown).execute(runnable);
task.run();
}
return init.build();
}
/**
* Returns the next item. Increases the counter.
*
* @return the next item
*/
public Item getNextItem() {
current++;
if (current >= items.size() - 1) {
current = 0;
}
return items.get(current);
}
@Override
public boolean isMovable() {
return false;
}
}

View file

@ -0,0 +1,403 @@
package dev.efnilite.vilib.inventory.item;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import dev.efnilite.vilib.util.Strings;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeModifier;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.Damageable;
import org.bukkit.inventory.meta.ItemMeta;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.function.Function;
/**
* A class for creating items.
*
* @author Efnilite
*/
@SuppressWarnings("unused")
public class Item extends MenuItem {
private int amount;
private int modelId = -1;
private int durability;
private boolean glowing = false;
private boolean unbreakable = false;
private String name;
private ItemMeta meta;
private Material material;
private List<String> lore = new ArrayList<>();
private Multimap<Attribute, AttributeModifier> attributes = HashMultimap.create();
private Map<Enchantment, Integer> enchantments = new HashMap<>();
/**
* Creates a new instance
*
* @param material The material
* @param name The name of the item
*/
public Item(Material material, String name) {
this(material, 1, name);
}
/**
* Creates a new instance
*
* @param material The material
* @param amount The amount of the item
* @param name The name of the item
*/
public Item(Material material, int amount, String name) {
this.amount = amount;
if (material != null) {
this.durability = material.getMaxDurability();
} else {
material = Material.GRASS_BLOCK;
}
this.name = name;
this.material = material;
}
@Override
public ItemStack build() {
ItemStack item = new ItemStack(material, amount);
if (meta == null) {
meta = Bukkit.getItemFactory().getItemMeta(item.getType());
}
// if this item's meta cant be modified, return itemstack instance
if (meta == null) {
return item;
}
if (glowing) {
meta.addEnchant(Enchantment.KNOCKBACK, 1, false);
meta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
} else {
for (Enchantment enchantment : enchantments.keySet()) {
meta.addEnchant(enchantment, enchantments.get(enchantment), true);
}
}
meta.setDisplayName(Strings.colour(name));
meta.setLore(Strings.colour(lore));
meta.setCustomModelData(modelId);
attributes.forEach(meta::addAttributeModifier);
((Damageable) meta).setDamage(Math.abs(durability - material.getMaxDurability()));
meta.setUnbreakable(unbreakable);
item.setItemMeta(meta);
return item;
}
/**
* Builds the meta for this item and passes it in f.
* The result of f is set as the meta for this item.
* The updated item is returned.
*
* @param f The function where the beginning item meta is provided and the final meta returned.
* @return This item with the returned item meta.
*/
public ItemStack build(Function<ItemMeta, ItemMeta> f) {
ItemStack item = build();
item.setItemMeta(f.apply(item.getItemMeta()));
return item;
}
@SuppressWarnings("all")
@Override
public Item clone() {
Item item = new Item(material, amount, name);
item.glowing = glowing;
item.durability = durability;
item.unbreakable = unbreakable;
item.meta = meta;
item.lore = lore;
item.attributes = attributes;
item.enchantments = enchantments;
return item;
}
@Override
public boolean isMovable() {
return false;
}
/**
* Set unbreakable
*
* @return the instance of this class
*/
public Item unbreakable() {
this.unbreakable = true;
return this;
}
/**
* Set glowing
*
* @return the instance of this class
*/
public Item glowing() {
this.glowing = true;
return this;
}
/**
* Set glowing
*
* @return the instance of this class
*/
public Item glowing(boolean predicate) {
if (predicate) {
this.glowing = true;
}
return this;
}
/**
* Sets the name
*
* @param name The name
* @return the instance of this class
*/
public Item name(String name) {
this.name = name;
return this;
}
/**
* Sets the ItemMeta
*
* @param meta The meta
* @return the instance of this class
*/
public Item meta(ItemMeta meta) {
this.meta = meta;
return this;
}
/**
* Sets the durability of the item
*
* @return the instance of this class
*/
public Item durability(int durability) {
this.durability = durability;
return this;
}
/**
* Sets the item amount
*
* @param amount The item amount
* @return the instance of this class
*/
public Item amount(int amount) {
this.amount = amount;
return this;
}
/**
* Sets the type
*
* @param material The type
* @return the instance of this class
*/
public Item material(Material material) {
this.material = material;
return this;
}
/**
* Sets the lore
*
* @param lore The lore
* @return the instance of this class
*/
public Item lore(@Nullable List<String> lore) {
if (lore != null) {
this.lore = lore;
}
return this;
}
/**
* Sets the lore
*
* @param lore The lore
* @return the instance of this class
*/
public Item lore(String... lore) {
return lore(List.of(lore));
}
// /**
// * Sets the lore supplier.
// *
// * @param supplier The lore supplier.
// * @return the instance of this class
// */
// public Item lore(@Nullable Supplier<List<String>> supplier) {
// if (supplier != null) {
// this.lore = lore;
// }
//
// return this;
// }
/**
* Enchants this item with a specific enchantment and a provided level.
* This ignores enchantment level limit restrictions.
*
* @param enchantment The enchantment instance.
* @param level The level.
* @return the instance of this class
*/
public Item enchant(@NotNull Enchantment enchantment, int level) {
enchantments.put(enchantment, level);
return this;
}
/**
* Adds the provided attribute to this item, with the specified value and the operation.
* Example:
* <br>
* <code>
* item.attribute(Attribute.GENERIC_ATTACK_SPEED, -10, AttributeModifier.Operation.ADD_NUMBER)
* </code>
*
* @param attribute The attribute
* @param value The value
* @param operation The operation
* @return the instance of this class
* @see AttributeModifier.Operation
*/
public Item attribute(@NotNull Attribute attribute, double value, AttributeModifier.Operation operation) {
attributes.put(attribute, new AttributeModifier(attribute.getKey().getKey(), value, operation));
return this;
}
/**
* Adds the provided attribute to this item, with the specified value and the operation, only applying to a specific slot.
*
* @param attribute The attribute
* @param value The value
* @param operation The operation
* @param slot The slot
* @return the instance of this class
* @see AttributeModifier.Operation
*/
public Item attribute(@NotNull Attribute attribute, double value, @NotNull AttributeModifier.Operation operation, @NotNull EquipmentSlot slot) {
attributes.put(attribute, new AttributeModifier(UUID.randomUUID(), attribute.getKey().getKey(), value, operation, slot));
return this;
}
/**
* Adds the provided {@link AttributeModifier} to the item with {@link Attribute} used as identification.
*
* @param attribute The attribute.
* @param modifier The modifier.
* @return the instance of this class
*/
public Item attribute(@NotNull Attribute attribute, @NotNull AttributeModifier modifier) {
attributes.put(attribute, modifier);
return this;
}
/**
* Sets this item's model id
*
* @param modelId The model id
* @return the instance of this class
*/
public Item modelId(int modelId) {
this.modelId = modelId;
return this;
}
/**
* Modifies the lore line by line.
* Useful for updating items.
*
* @param function The function. The line of lore is given, and it must return the modified version of that lore line
* @return the instance of this class
*/
public Item modifyLore(Function<String, String> function) {
this.lore = lore.stream()
.map(function)
.toList();
return this;
}
/**
* Modifies the name of an item.
* Useful for updating items.
*
* @param function The function. The title is given, and it must return an altered version of this title.
* @return the instance of this class
*/
public Item modifyName(Function<String, String> function) {
name = function.apply(name);
return this;
}
/**
* Gets the amount
*
* @return the amount
*/
public int getAmount() {
return amount;
}
/**
* Gets the lore
*
* @return the lore
*/
public List<String> getLore() {
return lore;
}
/**
* Gets the item type
*
* @return the type
*/
public Material getMaterial() {
return material;
}
/**
* Gets the name
*
* @return the name
*/
public String getName() {
return name;
}
}

View file

@ -0,0 +1,68 @@
package dev.efnilite.vilib.inventory.item;
import dev.efnilite.vilib.inventory.Menu;
import dev.efnilite.vilib.inventory.MenuClickEvent;
import org.bukkit.event.inventory.ClickType;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.inventory.ItemStack;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
/**
* Super class for every type of item which will be displayed in the menu
*
* @author Efnilite
*/
public abstract class MenuItem {
/**
* Prevent shift clicking from causing problems on inventory close
*/
private static final ClickType[] DEFAULT_TYPES = new ClickType[]{ClickType.LEFT, ClickType.RIGHT, ClickType.MIDDLE};
protected Map<ClickType, Consumer<MenuClickEvent>> clickFunctions = new HashMap<>();
/**
* Set the function on click
*
* @param consumer Useful values which are gathered in the click e vent
* @return the instance of this class
*/
public MenuItem click(Consumer<MenuClickEvent> consumer, ClickType... clickType) {
if (clickType.length == 0) {
for (ClickType type : DEFAULT_TYPES) {
clickFunctions.put(type, consumer);
}
} else {
for (ClickType type : clickType) {
clickFunctions.put(type, consumer);
}
}
return this;
}
public void handleClick(Menu menu, InventoryClickEvent event, ClickType clickType) {
Consumer<MenuClickEvent> consumer = clickFunctions.get(clickType);
if (consumer == null) {
return;
}
consumer.accept(new MenuClickEvent(event.getSlot(), menu, this, event));
}
/**
* Finishes everything and gives the ItemStack result.
*
* @return the result
*/
public abstract ItemStack build();
/**
* Whether this item can be moved by the player
*
* @return true -> it can, false -> it cant
*/
public abstract boolean isMovable();
}

View file

@ -0,0 +1,24 @@
package dev.efnilite.vilib.inventory.item;
import org.bukkit.Material;
/**
* An item which may be moved by the player
*
* @author Efnilite
*/
public class MovableItem extends Item {
public MovableItem(Material material, String name) {
super(material, name);
}
public MovableItem(Material material, int amount, String name) {
super(material, amount, name);
}
@Override
public boolean isMovable() {
return true;
}
}

View file

@ -0,0 +1,116 @@
package dev.efnilite.vilib.inventory.item;
import dev.efnilite.vilib.inventory.Menu;
import dev.efnilite.vilib.inventory.MenuClickEvent;
import dev.efnilite.vilib.util.Colls;
import org.bukkit.event.inventory.ClickType;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.inventory.ItemStack;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Predicate;
/**
* Class for an item which uses the difference between left/right click to assign values.
*
* @author Efnilite
*/
public class SliderItem extends MenuItem {
/**
* The initial value which will be displayed
*/
private int current;
private final Map<Integer, Item> items = new HashMap<>();
private final Map<Integer, Predicate<MenuClickEvent>> switchFunctions = new HashMap<>();
/**
* Sets the initial viewing index.
* If you set up 2 items, with index 0 and 1, you can specify which will be viewed first.
*
* @param initial The initial viewing index
* @return the instance of this class
*/
public SliderItem initial(int initial) {
this.current = initial;
return this;
}
/**
* Adds an item to the possible options. This uses a Function.
* The Function will determine whether the item will update in the inventory.
* If this returns false, it will not update the item in the menu, but it will execute the code.
*
* @param value The value assigned to this item
* @param item The item
* @param onSwitchTo What happens on switch to this item. Returns true if it should update, false if not.
* @return the instance of this class
*/
public SliderItem add(int value, Item item, Predicate<MenuClickEvent> onSwitchTo) {
items.put(value, item);
switchFunctions.put(value, onSwitchTo);
return this;
}
@Override
public void handleClick(Menu menu, InventoryClickEvent event, ClickType clickType) {
int currentTo = current;
switch (clickType) {
case LEFT:
currentTo++;
if (currentTo >= items.size()) {
currentTo = 0;
}
break;
case SHIFT_LEFT:
currentTo = items.size() - 1;
break;
case RIGHT:
currentTo--;
if (currentTo == -1) {
currentTo = items.size() - 1;
}
break;
case SHIFT_RIGHT:
currentTo = 0;
break;
default:
return;
}
Predicate<MenuClickEvent> function = switchFunctions.get(currentTo);
if (function == null) {
return;
}
boolean update = function.test(new MenuClickEvent(event.getSlot(), menu, this, event));
if (update) {
event.getInventory().setItem(event.getSlot(), items.get(currentTo).build());
current = currentTo;
menu.updateItem(event.getSlot());
}
}
@Override
public ItemStack build() {
if (items.keySet().size() == 0) {
throw new IllegalArgumentException("Items size is 0!");
}
Item init = items.get(current);
if (init == null) {
init = items.get(Colls.random(new ArrayList<>(items.keySet())));
}
return init.build();
}
@Override
public boolean isMovable() {
return false;
}
}

View file

@ -0,0 +1,84 @@
package dev.efnilite.vilib.inventory.item;
import dev.efnilite.vilib.inventory.Menu;
import dev.efnilite.vilib.inventory.MenuClickEvent;
import dev.efnilite.vilib.util.Task;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.ClickType;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.inventory.InventoryView;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitRunnable;
/**
* A MenuItem which only stays for a certain amount of ticks. This item is meant as a confirm option or to display items for a small amount of time.
* On click of this item it will automatically go back to the previous item.
*/
public class TimedItem extends MenuItem {
private int timeStay;
private Task task;
private MenuItem revertTo;
private final Plugin plugin;
private final Player player;
private final MenuItem item;
private final MenuClickEvent event;
public TimedItem(MenuItem item, MenuClickEvent event, Plugin plugin) {
this.item = item;
this.revertTo = event.menu().getItem(event.slot());
if (revertTo == null) {
revertTo = new Item(Material.AIR, "<red> ");
}
this.player = event.getPlayer();
this.event = event;
this.plugin = plugin;
}
/**
* Sets the amount of ticks this item will stay in view for.
*
* @param ticks The amount of ticks
* @return the instance of the class
*/
public TimedItem stay(int ticks) {
this.timeStay = ticks;
return this;
}
@Override
public ItemStack build() {
BukkitRunnable runnable = new BukkitRunnable() {
@Override
public void run() {
Menu menu = event.menu();
InventoryView view = player.getOpenInventory();
if (view.getTitle().equals(menu.getTitle())) {
menu.item(event.slot(), revertTo);
menu.updateItem(event.slot());
} else {
cancel(); // prevent going on forever
}
}
};
task = Task.create(plugin).delay(timeStay).execute(runnable);
task.run();
return item.build();
}
@Override
public void handleClick(Menu menu, InventoryClickEvent event, ClickType clickType) {
item.handleClick(menu, event, clickType);
task.cancelAndRunImmediately();
}
@Override
public boolean isMovable() {
return false;
}
}

View file

@ -0,0 +1,117 @@
package dev.efnilite.vilib.particle;
import org.bukkit.Particle;
import org.jetbrains.annotations.Nullable;
/**
* Wrapper for data used in {@link Particles}
*
* @author Efnilite
*/
public class ParticleData<T> {
private int size;
private double speed;
private double offsetX;
private double offsetY;
private double offsetZ;
private Particle type;
@Nullable
private T data;
public ParticleData(Particle type, @Nullable T data, int size) {
this(type, data, size, 0, 0, 0, 0);
}
public ParticleData(Particle type, @Nullable T data, int size, double speed) {
this(type, data, size, speed, 0, 0, 0);
}
public ParticleData(Particle type, @Nullable T data, int size, double offsetX, double offsetY, double offsetZ) {
this(type, data, size, 0, offsetX, offsetY, offsetZ);
}
/**
* Main constructor for Particle Data.
*
* @param type The type of particle
* @param data The possible data associated with this particle (colour, etc.)
* @param size The size of the particle
* @param speed The speed of the particle
* @param offsetX The max x offset in moving
* @param offsetY The max y offset in moving
* @param offsetZ The max z offset in moving
*/
public ParticleData(Particle type, @Nullable T data, int size, double speed, double offsetX, double offsetY, double offsetZ) {
this.data = data;
this.type = type;
this.size = size;
this.speed = speed;
this.offsetX = offsetX;
this.offsetY = offsetY;
this.offsetZ = offsetZ;
}
public ParticleData<T> size(int size) {
this.size = size;
return this;
}
public ParticleData<T> speed(double speed) {
this.speed = speed;
return this;
}
public ParticleData<T> offsetX(double offsetX) {
this.offsetX = offsetX;
return this;
}
public ParticleData<T> offsetY(double offsetY) {
this.offsetY = offsetY;
return this;
}
public ParticleData<T> offsetZ(double offsetZ) {
this.offsetZ = offsetZ;
return this;
}
public ParticleData<T> data(@Nullable T data) {
this.data = data;
return this;
}
public ParticleData<T> type(Particle type) {
this.type = type;
return this;
}
public int getSize() {
return size;
}
public double getSpeed() {
return speed;
}
public double getOffsetX() {
return offsetX;
}
public double getOffsetY() {
return offsetY;
}
public double getOffsetZ() {
return offsetZ;
}
public @Nullable T getData() {
return data;
}
public Particle getType() {
return type;
}
}

View file

@ -0,0 +1,230 @@
package dev.efnilite.vilib.particle;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.util.BoundingBox;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
/**
* Does particle stuff
*
* @author Efnilite
*/
public class Particles {
/**
* Draws particles.
* Uses default Bukkit World methods to spawn particles using {@link ParticleData}
*
* @param at The location of the particles
* @param data The particle data
*/
public static <T> void draw(Location at, @NotNull ParticleData<T> data) {
World world = at.getWorld();
if (world == null) {
throw new NullPointerException("World is null (Particles#draw)");
}
world.spawnParticle(data.getType(), at, data.getSize(), data.getOffsetX(), data.getOffsetY(), data.getOffsetZ(), data.getSpeed(), data.getData());
}
public static <T> void draw(Location at, @NotNull ParticleData<T> data, Player player) {
World world = at.getWorld();
if (world == null) {
throw new NullPointerException("World is null (Particles#draw)");
}
player.spawnParticle(data.getType(), at, data.getSize(), data.getOffsetX(), data.getOffsetY(), data.getOffsetZ(), data.getSpeed(), data.getData());
}
/**
* Draws a particle line between 2 points
* <p>
* First it finds the max and min locations, then it calculates the amount of particles based on param distanceBetween
* After that it gets the 3D distances (x, y and z) between the max and min points (delta X / amount of particles).
* <p>
* Then it loops the amount of times particles will spawn and in every loop removes delta X,
* delta Y and delta Z from the max location.
* </p><p>
* Example: max is (10, 10, 10), min is (0, 0, 0), distance between particles = 1 block.
* Gets distance between max and min (~ 14) and divides that by distance between (14 / 1 = 14 particles).
* Then the difference between max and min gets calculated (10 - 0 = 10) and that gets divided by amount (10 / 14 = ~ 0.7).
* This means that on the x, y and z axis every time the loop fires 0.7 blocks get removed. Looping 14 times and removing
* 0.7 every time, means it goes from (10, 10, 10) to (0, 0, 0) and gets all the locations inbetween those 2 locations, where
* particles need to be spawned.
* </p>
*
* @param one The location from where the tower shoots (always shoot variable)
* @param two The location of the entity
* @param data The particle data
* @param distanceBetween The distance between particles in blocks
*/
public static <T> void line(Location one, Location two, ParticleData<T> data, double distanceBetween) {
World world = one.getWorld();
if (world == null) {
throw new NullPointerException("World is null (Particles#draw)");
}
double dist = one.distance(two);
Vector p1 = one.toVector();
Vector p2 = two.toVector();
Vector vec = p2.clone().subtract(p1).normalize().multiply(distanceBetween);
world.spawnParticle(data.getType(), p1.getX(), p1.getY(), p1.getZ(), data.getSize(), data.getOffsetX(), data.getOffsetY(), data.getOffsetZ(), data.getSpeed(), data.getData());
world.spawnParticle(data.getType(), p2.getX(), p2.getY(), p2.getZ(), data.getSize(), data.getOffsetX(), data.getOffsetY(), data.getOffsetZ(), data.getSpeed(), data.getData());
double length = 0;
for (; length < dist; p1.add(vec)) {
world.spawnParticle(data.getType(), p1.getX(), p1.getY(), p1.getZ(), data.getSize(), data.getOffsetX(), data.getOffsetY(), data.getOffsetZ(), data.getSpeed(), data.getData());
length += distanceBetween;
}
}
/**
* Creates a box of particles
* Calculates the min point and then adds all the dimensions to each other point to get the locations of all the points
*
* @param box The box which the particles will go around
* @param world The world
* @param data The particle data
* @param distanceBetween The distance between particles
*/
public static <T> void box(BoundingBox box, @NotNull World world, ParticleData<T> data, Player player, double distanceBetween) {
Location point1 = box.getMin().toLocation(world);
Location point2, point3, point4, point5, point6, point7, point8;
if (box.getWidthX() == 1 && box.getWidthZ() == 1) {
point2 = point1.clone().add(box.getWidthX(), 0, 0);
point3 = point2.clone().add(0, 0, box.getWidthZ());
point4 = point1.clone().add(0, 0, box.getWidthZ());
point5 = point1.clone().add(0, box.getHeight(), 0);
point6 = point2.clone().add(0, box.getHeight(), 0);
point7 = point3.clone().add(0, box.getHeight(), 0);
point8 = point4.clone().add(0, box.getHeight(), 0);
} else {
point2 = point1.clone().add(box.getWidthX() + 1, 0, 0);
point3 = point2.clone().add(0, 0, box.getWidthZ() + 1);
point4 = point1.clone().add(0, 0, box.getWidthZ() + 1);
point5 = point1.clone().add(0, box.getHeight() + 1, 0);
point6 = point2.clone().add(0, box.getHeight() + 1, 0);
point7 = point3.clone().add(0, box.getHeight() + 1, 0);
point8 = point4.clone().add(0, box.getHeight() + 1, 0);
}
line(point1, point2, data, player, distanceBetween);
line(point2, point3, data, player, distanceBetween);
line(point3, point4, data, player, distanceBetween);
line(point4, point1, data, player, distanceBetween);
line(point5, point6, data, player, distanceBetween);
line(point6, point7, data, player, distanceBetween);
line(point7, point8, data, player, distanceBetween);
line(point5, point8, data, player, distanceBetween);
line(point1, point5, data, player, distanceBetween);
line(point2, point6, data, player, distanceBetween);
line(point3, point7, data, player, distanceBetween);
line(point4, point8, data, player, distanceBetween);
}
/**
* Creates a box of particles.
* Calculates the min point and then adds all the dimensions to each other point to get the locations of all the points
*
* @param box The box which the particles will go around
* @param world The world
* @param data The particle data
* @param distanceBetween The distance between particles
*/
public static <T> void box(BoundingBox box, @NotNull World world, ParticleData<T> data, double distanceBetween) {
Location point1 = box.getMin().toLocation(world);
Location point2, point3, point4, point5, point6, point7, point8;
if (box.getWidthX() == 1.0 && box.getWidthZ() == 1.0) {
point2 = point1.clone().add(box.getWidthX(), 0.0, 0.0);
point3 = point2.clone().add(0.0, 0.0, box.getWidthZ());
point4 = point1.clone().add(0.0, 0.0, box.getWidthZ());
point5 = point1.clone().add(0.0, box.getHeight(), 0.0);
point6 = point2.clone().add(0.0, box.getHeight(), 0.0);
point7 = point3.clone().add(0.0, box.getHeight(), 0.0);
point8 = point4.clone().add(0.0, box.getHeight(), 0.0);
} else {
point2 = point1.clone().add(box.getWidthX() + 1.0, 0.0, 0.0);
point3 = point2.clone().add(0.0, 0.0, box.getWidthZ() + 1.0);
point4 = point1.clone().add(0.0, 0.0, box.getWidthZ() + 1.0);
point5 = point1.clone().add(0.0, box.getHeight() + 1.0, 0.0);
point6 = point2.clone().add(0.0, box.getHeight() + 1.0, 0.0);
point7 = point3.clone().add(0.0, box.getHeight() + 1.0, 0.0);
point8 = point4.clone().add(0.0, box.getHeight() + 1.0, 0.0);
}
line(point1, point2, data, distanceBetween);
line(point2, point3, data, distanceBetween);
line(point3, point4, data, distanceBetween);
line(point4, point1, data, distanceBetween);
line(point5, point6, data, distanceBetween);
line(point6, point7, data, distanceBetween);
line(point7, point8, data, distanceBetween);
line(point5, point8, data, distanceBetween);
line(point1, point5, data, distanceBetween);
line(point2, point6, data, distanceBetween);
line(point3, point7, data, distanceBetween);
line(point4, point8, data, distanceBetween);
}
/**
* {@link #line(Location, Location, ParticleData, double)} but for players
*/
public static <T> void line(Location one, Location two, ParticleData<T> data, Player player, double distanceBetween) {
World world = one.getWorld();
if (world == null) {
throw new NullPointerException("World is null (Particles#draw)");
}
double dist = one.distance(two);
Vector p1 = one.toVector();
Vector p2 = two.toVector();
Vector vec = p2.clone().subtract(p1).normalize().multiply(distanceBetween);
player.spawnParticle(data.getType(), p1.getX(), p1.getY(), p1.getZ(), data.getSize(), data.getOffsetX(), data.getOffsetY(), data.getOffsetZ(), data.getSpeed(), data.getData());
player.spawnParticle(data.getType(), p2.getX(), p2.getY(), p2.getZ(), data.getSize(), data.getOffsetX(), data.getOffsetY(), data.getOffsetZ(), data.getSpeed(), data.getData());
double length = 0;
for (; length < dist; p1.add(vec)) {
player.spawnParticle(data.getType(), p1.getX(), p1.getY(), p1.getZ(), data.getSize(), data.getOffsetX(), data.getOffsetY(), data.getOffsetZ(), data.getSpeed(), data.getData());
length += distanceBetween;
}
}
/**
* Creates a circle
*
* @param location The center
* @param data The particle data
* @param radius The radius of the circle
* @param amount The amount of particles
*/
public static <T> void circle(Location location, ParticleData<T> data, double radius, double amount) {
World world = location.getWorld();
if (world == null) {
throw new NullPointerException("World is null");
}
double increment = (2 * Math.PI) / amount; // calc degree per amount, 2 x pi x r = circumference
double y = location.getY();
for (int i = 0; i < amount; i++) {
double angle = i * increment;
double x = location.getX() + (radius * Math.cos(angle));
double z = location.getZ() + (radius * Math.sin(angle));
world.spawnParticle(data.getType(), x, y, z, data.getSize(), data.getOffsetX(), data.getOffsetY(), data.getOffsetZ(), data.getSpeed(), data.getData());
}
}
public static <T> void circle(Location location, ParticleData<T> data, @NotNull Player player, double radius, double amount) {
double increment = (2 * Math.PI) / amount; // calc degree per amount, 2 x pi x r = circumference
double y = location.getY();
for (int i = 0; i < amount; i++) {
double angle = i * increment;
double x = location.getX() + (radius * Math.cos(angle));
double z = location.getZ() + (radius * Math.sin(angle));
player.spawnParticle(data.getType(), x, y, z, data.getSize(), data.getOffsetX(), data.getOffsetY(), data.getOffsetZ(), data.getSpeed(), data.getData());
}
}
}

View file

@ -0,0 +1,140 @@
package dev.efnilite.vilib.schematic;
import dev.efnilite.vilib.schematic.io.SchematicPaster;
import dev.efnilite.vilib.schematic.io.SchematicReader;
import dev.efnilite.vilib.schematic.io.SchematicWriter;
import dev.efnilite.vilib.util.Task;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.plugin.Plugin;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static java.lang.Math.max;
import static java.lang.Math.min;
/**
* Main schematic handling class.
*
* @author Efnilite
* @since 5.0.0
*/
public class Schematic {
/**
* The version of this library instance.
*/
public static final int VERSION = 1;
private final File file;
private final Map<Vector, BlockData> vectorBlockMap;
/**
* Constructor.
*
* @param file The file.
*/
public Schematic(@NotNull File file, @NotNull Plugin plugin) throws IOException, ClassNotFoundException {
this.file = file;
this.vectorBlockMap = new SchematicReader().read(file, plugin);
}
/**
* Pastes a schematic.
*
* @param location The smallest location.
*/
public List<Block> paste(Location location) {
return new SchematicPaster().paste(location, vectorBlockMap);
}
/**
* Pastes a schematic at angles rotation.
*
* @param location The smallest location.
* @param rotation The rotation where y = yaw in rad.
*/
public List<Block> paste(Location location, double rotation) {
return new SchematicPaster().paste(location, rotation, vectorBlockMap);
}
/**
* @return The dimensions of this schematic.
*/
public Vector getDimensions() {
Set<Vector> offsets = vectorBlockMap.keySet();
Vector min = offsets.stream().reduce((a, b) -> new Vector(min(a.getX(), b.getX()), min(a.getY(), b.getY()), min(a.getZ(), b.getZ()))).orElseThrow();
Vector max = offsets.stream().reduce((a, b) -> new Vector(max(a.getX(), b.getX()), max(a.getY(), b.getY()), max(a.getZ(), b.getZ()))).orElseThrow();
return max.subtract(min);
}
/**
* @return True when this schematic contains unknown {@link BlockData}, false if it does.
*/
public boolean hasUnknownMaterials() {
return vectorBlockMap.containsValue(null);
}
/**
* @return The map of vectors mapped to each {@link BlockData}.
*/
public Map<Vector, BlockData> getVectorBlockMap() {
return vectorBlockMap;
}
/**
* @return The file.
*/
public File getFile() {
return file;
}
/**
* Loads a schematic asynchronously.
*
* @param file The file.
* @return A new {@link Schematic} instance.
*/
public static Schematic load(File file, Plugin plugin) throws IOException, ClassNotFoundException {
return new Schematic(file, plugin);
}
/**
* Loads a schematic asynchronously.
*
* @param file The file.
* @return A new {@link Schematic} instance.
*/
public static Schematic load(String file, Plugin plugin) throws IOException, ClassNotFoundException {
return new Schematic(new File(file), plugin);
}
/**
* Saves the selection between the two locations asynchronously to file.
*
* @param file The file.
* @param pos1 The first position.
* @param pos2 The second position.
*/
public static void save(String file, Location pos1, Location pos2, Plugin plugin) {
save(new File(file), pos1, pos2, plugin);
}
/**
* Saves the selection between the two locations asynchronously to file.
*
* @param file The file.
* @param pos1 The first position.
* @param pos2 The second position.
*/
public static void save(File file, Location pos1, Location pos2, Plugin plugin) {
Task.create(plugin).async().execute(() -> new SchematicWriter().save(file, pos1, pos2, plugin)).run();
}
}

View file

@ -0,0 +1,40 @@
package dev.efnilite.vilib.schematic;
import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class Schematics {
private static final Map<Plugin, Map<String, Schematic>> cache = new HashMap<>();
public static void addFromFiles(@NotNull Plugin plugin, @NotNull File... files) throws IOException, ClassNotFoundException {
Map<String, Schematic> current = cache.getOrDefault(plugin, new HashMap<>());
for (File file : files) {
current.put(file.getName(), Schematic.load(file, plugin));
}
cache.put(plugin, current);
plugin.getLogger().info("Loaded all schematics!");
}
public static Schematic getSchematic(@NotNull Plugin plugin, @NotNull String schematicName) {
return cache.get(plugin).get(schematicName);
}
public static Set<String> getSchematicNames(@NotNull Plugin plugin) {
return cache.get(plugin).keySet();
}
public static Collection<Schematic> getSchematics(@NotNull Plugin plugin) {
return cache.get(plugin).values();
}
}

View file

@ -0,0 +1,102 @@
package dev.efnilite.vilib.schematic.io;
import dev.efnilite.vilib.util.Colls;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.Directional;
import org.bukkit.block.data.MultipleFacing;
import org.bukkit.util.Vector;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.function.Supplier;
/**
* Schematic pasting handler.
*
* @author Efnilite
* @since 5.0.0
*/
public class SchematicPaster {
/**
* Pastes a schematic with the provided map of offsets and BlockData.
*
* @param location The smallest location.
* @param vectorDataMap The map.
* @return The affected blocks.
*/
public List<Block> paste(Location location, Map<Vector, BlockData> vectorDataMap) {
return paste(() -> Colls.thread(vectorDataMap)
.mapk((k, v) -> location.clone().add(k).getBlock())
.get());
}
/**
* Pastes a schematic at angle rotation with the provided map of offsets and BlockData.
* Rotates {@link Directional} and {@link MultipleFacing} blocks.
*
* @param location The smallest location.
* @param rotation The rotation where y is the yaw in rad.
* @param vectorDataMap The map.
* @return The affected blocks.
*/
public List<Block> paste(Location location, double rotation, Map<Vector, BlockData> vectorDataMap) {
return paste(() -> Colls.thread(vectorDataMap)
.mapkv((vector) -> location.clone().add(round(vector.rotateAroundY(rotation))).getBlock(),
(data) -> {
if (data instanceof Directional directional) {
directional.setFacing(getClosest(directional.getFacing().getDirection(), rotation, directional.getFaces()));
return directional;
}
// todo add
// - Orientatable
// why are there so many?
return data;
})
.get());
}
private BlockFace getClosest(Vector direction, double rotation, Set<BlockFace> allowedFaces) {
Vector rotated = round(direction.clone().rotateAroundY(rotation));
return allowedFaces.stream()
.min(Comparator.comparingDouble(f -> f.getDirection().angle(rotated)))
.orElseThrow();
}
// rounds a vector to avoid small discrepancies like having an offset of E-16
private Vector round(Vector vector) {
double x = vector.getX();
double y = vector.getY();
double z = vector.getZ();
double epsilon = Vector.getEpsilon();
return new Vector(Math.abs(x) >= epsilon ? x : 0,
Math.abs(y) >= epsilon ? y : 0,
Math.abs(z) >= epsilon ? z : 0);
}
private List<Block> paste(Supplier<Map<Block, BlockData>> blocksGetter) {
try {
Map<Block, BlockData> blocks = CompletableFuture.supplyAsync(blocksGetter).get();
blocks.forEach((block, data) -> {
if (block == null || data == null) {
return;
}
block.setBlockData(data, false);
});
return new ArrayList<>(blocks.keySet());
} catch (InterruptedException | ExecutionException ex) {
return Collections.emptyList();
}
}
}

View file

@ -0,0 +1,59 @@
package dev.efnilite.vilib.schematic.io;
import dev.efnilite.vilib.util.Colls;
import org.bukkit.Bukkit;
import org.bukkit.block.data.BlockData;
import org.bukkit.plugin.Plugin;
import org.bukkit.util.Vector;
import java.io.*;
import java.util.Map;
/**
* Schematic reading handler.
*
* @author Efnilite
* @since 5.0.0
*/
public class SchematicReader {
/**
* @param file The file.
* @return A new {@link dev.efnilite.vilib.schematic.Schematic} instance based on the read blocks.
*/
@SuppressWarnings("unchecked")
public Map<Vector, BlockData> read(File file, Plugin plugin) throws IOException, ClassNotFoundException {
try (ObjectInputStream stream = new ObjectInputStream(new BufferedInputStream(new FileInputStream(file)))) {
var version = (int) stream.readObject();
var palette = (Map<String, Integer>) stream.readObject();
var offsets = (Map<String, Integer>) stream.readObject();
Map<Integer, BlockData> paletteRef = Colls.thread(palette).inverse().mapv((k, ov) -> {
try {
return Bukkit.createBlockData(ov);
} catch (IllegalArgumentException ex) {
try {
// < 1.19 versions for spawn schematic
if (ov.contains("leaves")) {
return Bukkit.createBlockData(ov.replaceAll(",?waterlogged=(false|true)", ""));
}
} catch (IllegalArgumentException ignored) {
}
plugin.getLogger().warning("Unknown block data %s in %s".formatted(ov, file.getName()));
return null;
}
}).get();
// create final map by parse Map<String, Object> -> Vector and applying possible State
return Colls.thread(offsets).mapkv(this::fromString, paletteRef::get).get();
}
}
private Vector fromString(String string) {
String[] parts = string.split(",");
return new Vector(Double.parseDouble(parts[0]), Double.parseDouble(parts[1]), Double.parseDouble(parts[2]));
}
}

View file

@ -0,0 +1,102 @@
package dev.efnilite.vilib.schematic.io;
import dev.efnilite.vilib.schematic.Schematic;
import dev.efnilite.vilib.util.Locations;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.plugin.Plugin;
import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Schematic writing handler.
*
* @author Efnilite
* @since 5.0.0
*/
public class SchematicWriter {
/**
* Saves blocks to the specified file.
*
* @param file The file.
* @param pos1 The first position.
* @param pos2 The second position.
*/
public void save(File file, Location pos1, Location pos2, Plugin plugin) {
List<Block> blocks = getBlocks(Locations.min(pos1, pos2), Locations.max(pos1, pos2));
Map<String, Integer> palette = getPalette(blocks);
Map<String, Integer> offsetData = getOffsetData(blocks, palette);
// write to file
try (ObjectOutputStream stream = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(file)))) {
stream.writeObject(Schematic.VERSION);
stream.writeObject(palette);
stream.writeObject(offsetData);
stream.flush();
} catch (IOException ex) {
ex.printStackTrace();
}
}
// returns all blocks between the min location (minL) and max location (maxL)
private List<Block> getBlocks(Location minL, Location maxL) {
List<Block> blocks = new ArrayList<>();
Location location = new Location(minL.getWorld() == null ? maxL.getWorld() : minL.getWorld(), 0, 0, 0);
for (int x = minL.getBlockX(); x <= maxL.getBlockX(); x++) {
for (int y = minL.getBlockY(); y <= maxL.getBlockY(); y++) {
for (int z = minL.getBlockZ(); z <= maxL.getBlockZ(); z++) {
location.setX(x);
location.setY(y);
location.setZ(z);
if (location.getBlock().getType() == Material.AIR) {
continue;
}
blocks.add(location.getBlock());
}
}
}
return blocks;
}
// returns block palette where ids are used instead of the full blockdata to reduce file space
private Map<String, Integer> getPalette(List<Block> blocks) {
Map<String, Integer> palette = new HashMap<>();
blocks.stream()
.map(block -> block.getBlockData().getAsString())
.distinct()
.forEach(datum -> palette.put(datum, palette.size()));
return palette;
}
// returns the offset data, where each block offset (position relative to minimum location)
// is mapped to the data at that position.
// the first item in the array is the generic BlockData of that block.
// the second item in the array is the special data of that block, as encoded by the State instances.
private Map<String, Integer> getOffsetData(List<Block> blocks, Map<String, Integer> palette) {
List<Location> locations = blocks.stream().map(Block::getLocation).toList();
Location min = locations.stream().reduce(Locations::min).orElseThrow();
Map<String, Integer> offsetData = new HashMap<>();
for (Location loc : locations) {
offsetData.put(loc.clone().subtract(min).toVector().toString(),
palette.get(loc.clone().getBlock().getBlockData().getAsString()));
}
return offsetData;
}
}

View file

@ -0,0 +1,173 @@
package dev.efnilite.vilib.util;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public final class Colls {
/**
* Creates a util class to simplify map threading.
*
* @param map The map.
* @param <K> The key type.
* @param <V> The value type.
* @return A new {@link MapThreader}.
*/
public static <K, V> MapThreader<K, V> thread(Map<K, V> map) {
return new MapThreader<>(map);
}
/**
* Threads through maps. Modifications return {@link HashMap}.
*
* @param <K> The key type.
* @param <V> The value type.
*/
public static class MapThreader<K, V> {
private Map<K, V> map;
public MapThreader(Map<K, V> map) {
this.map = map;
}
public Map<K, V> get() {
return map;
}
/**
* @param f The function to apply.
* @return Threader with map, only with the items where f returns true. Returns {@link HashMap}.
*/
public MapThreader<K, V> filter(BiFunction<K, V, Boolean> f) {
Map<K, V> newMap = new HashMap<>();
for (Map.Entry<K, V> entry : map.entrySet()) {
if (f.apply(entry.getKey(), entry.getValue())) {
newMap.put(entry.getKey(), entry.getValue());
}
}
map = newMap;
return this;
}
/**
* @param f The function to apply to each value in collection. Arguments are the key and original value.
* @param <NV> The new list type.
* @return Threader with map, where f has been applied to each value in map. Returns {@link HashMap}.
*/
public <NV> MapThreader<K, NV> mapv(BiFunction<K, V, NV> f) {
Map<K, NV> newMap = new HashMap<>();
map.forEach((key, value) -> newMap.put(key, f.apply(key, value)));
return new MapThreader<>(newMap);
}
/**
* @param f The function to apply to each key in collection. Arguments are the original key and value.
* @param <NK> The new key type.
* @return Threader with map, where f has been applied to each key in map. Returns {@link HashMap}.
*/
public <NK> MapThreader<NK, V> mapk(BiFunction<K, V, NK> f) {
Map<NK, V> newMap = new HashMap<>();
map.forEach((key, value) -> newMap.put(f.apply(key, value), value));
return new MapThreader<>(newMap);
}
/**
* Maps kf to each key and vf to each value. Modifications of k/v are independent of each other but executed at the same time.
*
* @param kf The function to apply to each key in collection. Argument is the original key.
* @param vf The function to apply to each value in collection. Argument is the original value.
* @param <NK> The new key type.
* @param <NV> The new value type.
* @return Threader with map, where kf has been applied to each key and vf to each value. Returns {@link HashMap}.
*/
public <NK, NV> MapThreader<NK, NV> mapkv(Function<K, NK> kf, Function<V, NV> vf) {
Map<NK, NV> newMap = new HashMap<>();
map.forEach((key, value) -> newMap.put(kf.apply(key), vf.apply(value)));
return new MapThreader<>(newMap);
}
/**
* Performs consumer for every key and value.
*
* @param consumer The consumer.
*/
public void forEach(BiConsumer<K, V> consumer) {
map.forEach(consumer);
}
/**
* Maps all keys to values.
*
* @return Threader with map, where every value is the key to each key. Returns {@link HashMap}.
*/
public MapThreader<V, K> inverse() {
Map<V, K> newMap = new HashMap<>();
map.forEach((k, v) -> newMap.put(v, k));
return new MapThreader<>(newMap);
}
}
/**
* @param coll The collection.
* @param <T> The type.
* @return A random item from coll.
*/
public static <T> T random(List<T> coll) {
return coll.get(ThreadLocalRandom.current().nextInt(coll.size()));
}
/**
* @param coll The collection.
* @param n The amount of random items.
* @param <T> The type.
* @return n random items from coll.
*/
public static <T> List<T> random(List<T> coll, int n) {
return IntStream.range(0, n).mapToObj(i -> random(coll)).collect(Collectors.toList());
}
/**
* @param start The start. Inclusive.
* @param end The end. Exclusive.
* @param step The increment between values.
* @return List with all ints from start to end with increment step.
*/
public static List<Integer> range(int start, int end, int step) {
return IntStream.iterate(start, i -> i < end, i -> i + step).boxed().collect(Collectors.toList());
}
/**
* @param start The start. Inclusive.
* @param end The end. Exclusive.
* @return List with all ints from start to end with increment 1.
*/
public static List<Integer> range(int start, int end) {
return range(start, end, 1);
}
/**
* @param end The end. Exclusive.
* @return List with all ints from 0 to end with increment 1.
*/
public static List<Integer> range(int end) {
return range(0, end, 1);
}
}

View file

@ -0,0 +1,118 @@
package dev.efnilite.vilib.util;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* Utility class for handling cooldowns. Cooldowns are stored by key (and optionally by player).
* The keys are a way of identifying which action to use in the cooldown.
* When a player performs action A, action B and then action B again, the system needs to be able to differentiate action A and B,
* since a player can be on cooldown for multiple things.
* <p>Using the {@link #canPerform(String, long)} or {@link #canPerform(Player, String, long)} methods allows you to check
* whether a player can perform the action specified in the arguments. These methods return true when the player
* is allowed to perform the action, and false when the player is not. When the player is allowed to perform the action,
* the last execution time of the player is automatically updated to now. The {@code cooldown} argument is used to specify
* the cooldown for the provided key. Code example:</p>
* <blockquote><code>if (!canPerform(player, "right click wand", 1000)) { return; }</code></blockquote>
* <p>In the above example, the {@code return} statement will be executed when the player has performed the action {@code "right click wand"}
* in the last 1000 ms (1 second). If the action was last performed more than 1000 ms ago, the {@code return} statement will not be called.</p>
*/
public class Cooldowns {
private static final Map<String, Long> EXECUTION_TIMES = new HashMap<>();
/**
* Returns whether the provided action {@code key} can be performed with the provided cooldown {@code cooldown}.
* <strong>This method is only recommended to be used with global actions</strong>, e.g. requiring no player.
* Returns:
* <ul>
* <li>{@code true} - When the action with the provided key can be performed.
* The last execution was more than {@code cooldown} milliseconds ago.</li>
* <li>{@code false} - When the action with the provided key cannot be performed.
* The last execution was less than {@code cooldown} milliseconds ago.</li>
* </ul>
*
* @param key The key by which the system can differentiate actions.
* @param cooldown The cooldown in milliseconds.
* @return true if the action {@code key} was more than {@code cooldown} ms ago, false if not.
*/
public static boolean canPerform(@NotNull String key, long cooldown) {
Objects.requireNonNull(key);
Long lastTime = EXECUTION_TIMES.get(key);
// if no value has been previously registered, player is executing for the first time, so return true
if (lastTime == null) {
EXECUTION_TIMES.put(key, System.currentTimeMillis());
return true;
}
// get the difference between now and the last time the command was executed
long dt = System.currentTimeMillis() - lastTime;
// if the last time execution difference is higher than cooldown, allow execution
if (dt > cooldown) {
EXECUTION_TIMES.put(key, System.currentTimeMillis());
return true;
}
return false;
}
/**
* Returns whether the provided action {@code key}, belonging to player {@code player}, can be performed
* with the provided cooldown {@code cooldown}. Returns:
* <ul>
* <li>{@code true} - When the action with the provided player's key can be performed.
* The last execution was more than {@code cooldown} milliseconds ago.</li>
* <li>{@code false} - When the action with the provided player's key cannot be performed.
* The last execution was less than {@code cooldown} milliseconds ago.</li>
* </ul>
*
* @param key The key by which the system can differentiate actions.
* @param cooldown The cooldown in milliseconds.
* @return true if the action {@code key} was more than {@code cooldown} ms ago, false if not.
*/
public static boolean canPerform(@NotNull Player player, @NotNull String key, long cooldown) {
Objects.requireNonNull(player);
Objects.requireNonNull(key);
return canPerform(player.getUniqueId() + "-" + key, cooldown);
}
/**
* Returns the last registered execution time (in ms) of the specified string key. Can be null.
* <strong>This method is only recommended to be used with global actions</strong>, e.g. requiring no player.
*
* @param key The key by which the system can differentiate actions.
* @return the last execution time (in ms) of the global action. Can be null.
*/
@Nullable
public static Long getLastExecutionTime(@NotNull String key) {
Objects.requireNonNull(key);
return EXECUTION_TIMES.get(key);
}
/**
* Returns the last registered execution time (in ms) of the specified string key that's associated to the provided player.
* Can be null.
*
* @param player The player.
* @param key The key by which the system can differentiate actions.
* @return the last execution time (in ms) of the player. Can be null.
*/
@Nullable
public static Long getLastExecutionTime(@NotNull Player player, @NotNull String key) {
Objects.requireNonNull(player);
Objects.requireNonNull(key);
return EXECUTION_TIMES.get(player.getUniqueId() + "-" + key);
}
}

View file

@ -0,0 +1,110 @@
package dev.efnilite.vilib.util;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.type.Fence;
import org.bukkit.block.data.type.Wall;
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitRunnable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.function.Consumer;
/**
* Asynchronously gets the {@link Block} instances between the two specified {@link Location} instances.
* Executes the provided {@link Consumer} with the gathered block list.
*/
public class Cuboid {
/**
* The amount of changes per tick.
*/
public static final int CHANGES_PER_TICK = 2500;
/**
* Sets blocks in <code>blocks</code> to their respective {@link BlockData}.
* Performs <code>onComplete</code> when block setting has finished.
*
* @param blocks The block map.
* @param onComplete What to do on completion.
*/
public static void set(@NotNull Map<Block, BlockData> blocks, Plugin plugin, @Nullable Runnable onComplete) {
Queue<Block> queue = new LinkedList<>(blocks.keySet());
Task.create(plugin).repeat(1).execute(new BukkitRunnable() {
@Override
public void run() {
for (int i = 0; i < CHANGES_PER_TICK; i++) {
Block block = queue.poll();
if (block == null) {
// no blocks left, so cancel task
cancel();
if (onComplete != null) {
onComplete.run();
}
return;
}
setBlock(block, blocks.get(block));
}
}
}).run();
}
private static void setBlock(Block block, BlockData data) {
block.setBlockData(data, data instanceof Fence || data instanceof Wall);
}
/**
* Returns all blocks between the provided locations.
*
* @param pos1 The first location
* @param pos2 The second location
*/
public static List<Block> get(@NotNull Location pos1, @NotNull Location pos2, boolean ignoreAir) {
List<Block> blocks = new ArrayList<>();
Location max = Locations.max(pos1, pos2);
Location min = Locations.min(pos1, pos2);
Location location = max.clone();
location.setWorld(pos1.getWorld() == null ? pos2.getWorld() : pos1.getWorld());
for (int x = min.getBlockX(); x <= max.getBlockX(); x++) {
for (int y = min.getBlockY(); y <= max.getBlockY(); y++) {
for (int z = min.getBlockZ(); z <= max.getBlockZ(); z++) {
location.setX(x);
location.setY(y);
location.setZ(z);
if (ignoreAir) {
if (location.getBlock().getType() == Material.AIR) {
continue;
}
}
blocks.add(location.getBlock());
}
}
}
return blocks;
}
/**
* Returns all blocks between the provided locations asynchronously.
*
* @param pos1 The first location
* @param pos2 The second location
* @param onComplete A {@link Consumer} with the list of gathered blocks.
*/
public static void getAsync(@NotNull Location pos1, @NotNull Location pos2, boolean ignoreAir,
Plugin plugin, @NotNull Consumer<List<Block>> onComplete) {
Task.create(plugin).async().execute(() -> onComplete.accept(get(pos1, pos2, ignoreAir))).run();
}
}

View file

@ -0,0 +1,72 @@
package dev.efnilite.vilib.util;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
/**
* Utilities for Locations
*/
public class Locations {
/**
* Gets the max of the locations
*
* @param pos1 The first location
* @param pos2 The second location
* @return the max values of the locations
*/
public static Location max(Location pos1, Location pos2) {
World world = pos1.getWorld() == null ? pos2.getWorld() : pos1.getWorld();
return new Location(world, Math.max(pos1.getX(), pos2.getX()), Math.max(pos1.getY(), pos2.getY()), Math.max(pos1.getZ(), pos2.getZ()));
}
/**
* Gets the min of the locations
*
* @param pos1 The first location
* @param pos2 The second location
* @return the min values of the locations
*/
public static Location min(Location pos1, Location pos2) {
World world = pos1.getWorld() == null ? pos2.getWorld() : pos1.getWorld();
return new Location(world, Math.min(pos1.getX(), pos2.getX()), Math.min(pos1.getY(), pos2.getY()), Math.min(pos1.getZ(), pos2.getZ()));
}
/**
* Creates a string version of a Location.
*
* @param location The location
* @return string version
*/
public static String toString(Location location, boolean formatted) {
if (!formatted) {
return "(%s,%s,%s,%s)".formatted(location.getX(), location.getY(), location.getZ(), location.getWorld().getName());
} else {
return "(%d, %d, %d)".formatted(location.getBlockX(), location.getBlockY(), location.getBlockZ());
}
}
/**
* Get a location from a string
*
* @param string The string
* @return the location from the string
*/
public static Location parseLocation(String string) {
String[] values = string.replaceAll("[()]", "")
.replace(", ", " ")
.replace(",", " ")
.split(" ");
World world = Bukkit.getWorld(values[3]);
// if world is null, get first world
if (world == null) {
world = Bukkit.getWorlds().get(0);
}
return new Location(world, Double.parseDouble(values[0]), Double.parseDouble(values[1]), Double.parseDouble(values[2]));
}
}

View file

@ -0,0 +1,75 @@
package dev.efnilite.vilib.util;
import dev.efnilite.vilib.ViPlugin;
import org.jetbrains.annotations.Nullable;
import java.util.logging.Logger;
/**
* Adds useful data for later (e.g. game testing)
*/
public class Logging {
private final Logger logger;
private final ViPlugin plugin;
public Logging(ViPlugin plugin) {
this.plugin = plugin;
this.logger = plugin.getLogger();
}
public void info(String info) {
logger.info(info);
}
public void warn(String warn) {
logger.warning(warn);
}
public void error(String error) {
logger.severe(error);
}
public void stack(String error, String fix) {
stack(error, fix, null);
}
public void stack(String error, Throwable throwable) {
stack(error, null, throwable);
}
public void stack(String error, @Nullable String fix, @Nullable Throwable throwable) {
error("##");
error("## %s has encountered a critical error!".formatted(plugin.getName()));
error("## %s".formatted(error));
error("##");
if (throwable == null) {
error("## No stack trace provided");
} else {
error("## Stack trace:");
error("## %s".formatted(throwable));
StackTraceElement[] stack = throwable.getStackTrace();
for (StackTraceElement stackTraceElement : stack) {
error("##\t" + stackTraceElement.toString());
}
}
error("##");
if (fix == null) {
error("## This is probably not your fault.");
error("## Contact the developer to fix it.");
error("## Be sure to send the entire error while reporting.");
} else {
error("## This is probably not your fault, but you may be able to fix it.");
error("## You should try: %s".formatted(fix));
error("## Contact the developer if this doesn't work.");
error("## Be sure to send the entire error while reporting.");
}
error("##");
error("## Version information:");
error("##\tPlugin Version: %s".formatted(plugin.getDescription().getVersion()));
error("##\tMinecraft: %s".formatted(Version.getVersion()));
error("##");
}
}

View file

@ -0,0 +1,65 @@
package dev.efnilite.vilib.util;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
/**
* Number utils
*/
public class Numbers {
/**
* Returns a random real number between two bounds.
*
* @param lower The lower bound (inclusive)
* @param upper The upper bound (exclusive)
* @return a random real number between the two bounds
*/
public static int random(int lower, int upper) {
return ThreadLocalRandom.current().nextInt(lower, upper);
}
/**
* Gets the min int from an Array of ints
*
* @param ints All ints
* @return the smallest of the values of parameter ints
*/
public static int min(int... ints) {
return Arrays.stream(ints).min().orElseThrow();
}
/**
* Gets the max int from an array of ints
*
* @param ints All ints
* @return the int with the biggest value
*/
public static int max(int... ints) {
return Arrays.stream(ints).max().orElseThrow();
}
/**
* Returns all real numbers between two bounds
*
* @param from The lower bound (inclusive)
* @param to The upper bound (inclusive)
* @return a list of all real numbers between the two bounds
*/
public static List<Integer> getFromTo(int from, int to) {
return IntStream.rangeClosed(min(from, to), max(from, to)).boxed().collect(Collectors.toList());
}
/**
* Gets all ints from 0 (inclusive) to n (inclusive)
*
* @param n The max value
* @return a list with all ints from 0 to n
*/
public static List<Integer> getFromZero(int n) {
return getFromTo(0, n);
}
}

View file

@ -0,0 +1,77 @@
package dev.efnilite.vilib.util;
import org.jetbrains.annotations.NotNull;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
public final class Probs {
/**
* @return A random chance with range 0 (inclusive) to 1 (exclusive).
*/
public static double random() {
return random(ThreadLocalRandom.current());
}
/**
* @param random The random instance.
* @return A random chance with range 0 (inclusive) to 1 (exclusive).
*/
public static double random(Random random) {
return random.nextDouble();
}
/**
* @param mean The average value.
* @param sd The standard deviation.
* @param x The x-coordinate.
* @return The normal distributed chance at x.
*/
public static double normalpdf(double mean, double sd, double x) {
double a = (x - mean) / sd;
return (1 / (sd * Math.sqrt(2 * Math.PI))) * Math.exp(-0.5 * (a * a));
}
/**
* @param distribution A map where each key is mapped to a probability.
* @param random The random instance.
* @param <K> The key type.
* @return A random item from the list, based on the probabilities.
*/
public static <K> K random(@NotNull Map<K, Double> distribution, Random random) {
if (distribution.isEmpty()) {
throw new NoSuchElementException("No elements in distribution map");
}
double total = 0;
Map<K, Double> cumulative = new LinkedHashMap<>();
for (Map.Entry<K, Double> entry : distribution.entrySet()) {
total += entry.getValue();
cumulative.put(entry.getKey(), total);
}
double randomNum = random(random) * total;
for (Map.Entry<K, Double> entry : cumulative.entrySet()) {
if (entry.getValue() >= randomNum) {
return entry.getKey();
}
}
return distribution.keySet().stream().findFirst().orElseThrow();
}
/**
* @param distribution A map where each key is mapped to a probability.
* @param <K> The key type.
* @return A random item from the list, based on the probabilities.
*/
public static <K> K random(@NotNull Map<K, Double> distribution) {
return random(distribution, ThreadLocalRandom.current());
}
}

View file

@ -0,0 +1,44 @@
package dev.efnilite.vilib.util;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.bukkit.inventory.meta.SkullMeta;
import java.lang.reflect.Method;
public class SkullSetter {
private static boolean isPaper;
private static Method getPlayerProfileMethod;
private static Method hasTexturesMethod;
private static Method setPlayerProfileMethod;
static {
try {
Class<?> playerProfileClass = Class.forName("com.destroystokyo.paper.profile.PlayerProfile");
getPlayerProfileMethod = Player.class.getDeclaredMethod("getPlayerProfile");
hasTexturesMethod = playerProfileClass.getDeclaredMethod("hasTextures");
setPlayerProfileMethod = SkullMeta.class.getDeclaredMethod("setPlayerProfile", playerProfileClass);
isPaper = true;
} catch (Exception ex) {
isPaper = false;
}
}
public static void setPlayerHead(OfflinePlayer player, SkullMeta meta) {
if (!isPaper) { // ew
meta.setOwningPlayer(player);
return;
}
try {
Object playerProfile = getPlayerProfileMethod.invoke(player);
boolean hasTexture = (boolean) hasTexturesMethod.invoke(playerProfile);
if (hasTexture) {
setPlayerProfileMethod.invoke(meta, playerProfile);
}
} catch (Exception ex) {
meta.setOwningPlayer(player);
}
}
}

View file

@ -0,0 +1,128 @@
package dev.efnilite.vilib.util;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.standard.StandardTags;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import org.bukkit.ChatColor;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.stream.Collectors;
public class Strings {
private static final MiniMessage MINI_MESSAGE = MiniMessage.miniMessage();
private static final LegacyComponentSerializer LEGACY_COMPONENT_SERIALIZER = LegacyComponentSerializer.builder()
.extractUrls()
.hexColors()
.character(LegacyComponentSerializer.SECTION_CHAR)
.useUnusualXRepeatedCharacterHexFormat() // spigot makes me sad :(
.build();
/**
* Colours a list of strings using {@link MiniMessage} and {@link LegacyComponentSerializer}
*
* @param strings The list of strings
* @return the coloured list of strings
*/
public static List<String> colour(@NotNull List<String> strings) {
return strings.stream().map(Strings::colour).collect(Collectors.toList());
}
/**
* Colours an array of strings using {@link MiniMessage} and {@link LegacyComponentSerializer}
*
* @param strings The array of strings
* @return the array of strings, but coloured
*/
@NotNull
public static String[] colour(@NotNull String... strings) {
String[] coloured = new String[strings.length];
int index = 0;
for (String string : strings) {
coloured[index] = colour(string);
index++;
}
return coloured;
}
/**
* Colours a string using {@link MiniMessage} and {@link LegacyComponentSerializer}
*
* @param string The string
* @return the coloured string
*/
@NotNull
public static String colour(@NotNull String string) {
Component component = MINI_MESSAGE.deserialize(string); // sanitize input
return ChatColor.translateAlternateColorCodes(LegacyComponentSerializer.SECTION_CHAR, LEGACY_COMPONENT_SERIALIZER.serialize(component));
}
/**
* Gets the closest matching string
*
* @param source The source string
* @param strings Strings which will be compared to this string
* @return the closest matching string from parameter strings
*/
public static String getClosestMatching(String source, List<String> strings) {
int min = Integer.MAX_VALUE;
String closest = "";
for (String string : strings) {
int distance = getLevenshteinDistance(source, string);
if (distance < min) {
closest = string;
}
}
return closest;
}
/**
* Gets the levenshtein distance between two strings
* Source: <a href="https://www.stephenenright.com/java-levenshtein-distance">https://www.stephenenright.com/java-levenshtein-distance</a>
*
* @param source The source string
* @param other The other string
* @return the distance required
*/
public static int getLevenshteinDistance(String source, String other) {
int sourceLength = source.length();
int otherLength = other.length();
int[][] minDistanceMatrix = new int[sourceLength + 1][otherLength + 1]; // init the minimum distance matrix and add one to account for default values
minDistanceMatrix[0][0] = 0;
for (int row = 1; row <= sourceLength; row++) { // enter default edit values for the source string (in rows)
minDistanceMatrix[row][0] = row;
}
for (int col = 1; col <= otherLength; col++) { // enter default edit values for the other string (in cols)
minDistanceMatrix[0][col] = col;
}
for (int row = 1; row <= sourceLength; row++) {
for (int col = 1; col <= otherLength; col++) { // go through every value and get the min value
minDistanceMatrix[row][col] = getMinLevenshteinCost(source, other, minDistanceMatrix, row, col);
}
}
return minDistanceMatrix[sourceLength][otherLength]; // get the last value
}
private static int getMinLevenshteinCost(String source, String other, int[][] minDistanceMatrix, int row, int col) {
int insertion = minDistanceMatrix[row][col - 1] + 1;
int deletion = minDistanceMatrix[row - 1][col] + 1;
int substition = minDistanceMatrix[row - 1][col - 1];
if (source.charAt(row - 1) != other.charAt(col - 1)) { // if the letters are the same skip adding a cost
substition += 1;
}
return Numbers.min(insertion, deletion, substition);
}
}

View file

@ -0,0 +1,164 @@
package dev.efnilite.vilib.util;
import org.bukkit.Bukkit;
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitRunnable;
import org.bukkit.scheduler.BukkitScheduler;
import org.bukkit.scheduler.BukkitTask;
/**
* Class for easily constructing tasks.
*
* @author Efnilite
*/
public class Task {
private int delay = 0;
private int repeat = 0;
private boolean async = false;
private final Plugin plugin;
private Runnable defaultRunnable;
private BukkitTask task;
private BukkitRunnable bukkitRunnable;
public Task(Plugin plugin) {
this.plugin = plugin;
}
/**
* Returns a new Task instance
*
* @param plugin The plugin which to register this Task with
* @return the created Task instance
*/
public static Task create(Plugin plugin) {
return new Task(plugin);
}
/**
* Specifies which Java Runnable should be executed. This supports lambdas.
*
* @param runnable The Java Runnable that is going to be run
* @return the instance of the class
*/
public Task execute(Runnable runnable) {
this.defaultRunnable = runnable;
return this;
}
/**
* Specifies which Bukkit Runnable should be executed
*
* @param runnable The Bukkit Runnable that is going to be run
* @return the instance of the class
*/
public Task execute(BukkitRunnable runnable) {
this.bukkitRunnable = runnable;
return this;
}
/**
* Whether this task should run async
*
* @return the instance of this class
*/
public Task async() {
this.async = true;
return this;
}
/**
* The delay this task will run with
*
* @param delay The delay in ticks
* @return the instance of this class
*/
public Task delay(int delay) {
this.delay = delay;
return this;
}
/**
* The repeating interval this task will run with
*
* @param repeat The interval in ticks
* @return the instance of this class
*/
public Task repeat(int repeat) {
this.repeat = repeat;
return this;
}
/**
* Cancels this task
*
* @return the instance of this class
*/
public Task cancel() {
task.cancel();
return this;
}
/**
* Cancels the active/waiting task and runs the task immediately
*/
public void cancelAndRunImmediately() {
task.cancel();
if (bukkitRunnable != null) {
bukkitRunnable.run();
}
if (defaultRunnable != null) {
defaultRunnable.run();
}
}
/**
* Runs this task
*
* @return the BukkitTask instance returned from running this task
*/
public BukkitTask run() {
if (bukkitRunnable != null) {
if (async) { // async
if (repeat > 0) {
task = bukkitRunnable.runTaskTimerAsynchronously(plugin, delay, repeat);
} else if (delay > 0) {
task = bukkitRunnable.runTaskLaterAsynchronously(plugin, delay);
} else {
task = bukkitRunnable.runTaskAsynchronously(plugin);
}
} else {
if (repeat > 0) {
task = bukkitRunnable.runTaskTimer(plugin, delay, repeat);
} else if (delay > 0) {
task = bukkitRunnable.runTaskLater(plugin, delay);
} else {
task = bukkitRunnable.runTask(plugin);
}
}
} else if (defaultRunnable != null) {
BukkitScheduler scheduler = Bukkit.getScheduler();
if (async) { // async
if (repeat > 0) {
task = scheduler.runTaskTimerAsynchronously(plugin, defaultRunnable, delay, repeat);
} else if (delay > 0) {
task = scheduler.runTaskLaterAsynchronously(plugin, defaultRunnable, delay);
} else {
task = scheduler.runTaskAsynchronously(plugin, defaultRunnable);
}
} else {
if (repeat > 0) {
task = scheduler.runTaskTimer(plugin, defaultRunnable, delay, repeat);
} else if (delay > 0) {
task = scheduler.runTaskLater(plugin, defaultRunnable, delay);
} else {
task = scheduler.runTask(plugin, defaultRunnable);
}
}
} else {
throw new IllegalStateException("Both runnable types are null!");
}
return task;
}
}

View file

@ -0,0 +1,71 @@
package dev.efnilite.vilib.util;
import java.util.HashMap;
/**
* Time utils
*
* @author Efnilite
*/
public class Time {
private static final HashMap<String, Long> timings = new HashMap<>();
/**
* Hours per day.
*/
public static final int HOURS_PER_DAY = 24;
/**
* Minutes per hour.
*/
public static final int MINUTES_PER_HOUR = 60;
/**
* Seconds per minute.
*/
public static final int SECONDS_PER_MINUTE = 60;
/**
* Seconds per hour.
*/
public static final int SECONDS_PER_HOUR = 3600;
/**
* Seconds per day.
*/
public static final int SECONDS_PER_DAY = 86400;
/**
* Converts an amount of seconds to millis
*
* @param secs The second count
* @return the second count to millis
*/
public static long toMillis(long secs) {
return secs * 1000;
}
/**
* Start the timer.
*
* @param key The name of the operation which will be timed. These need to be unique!
*/
public static void timerStart(String key) {
timings.put(key, System.currentTimeMillis());
}
/**
* End the timer and get the time between start and finish in ms.
*
* @param key The name of the operation.
* @return the time it took between starting and finishing.
*/
public static long timerEnd(String key) {
long startTime = timings.get(key);
timings.remove(key);
return System.currentTimeMillis() - startTime;
}
}

View file

@ -0,0 +1,56 @@
package dev.efnilite.vilib.util;
import org.bukkit.plugin.Plugin;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class UpdateChecker {
public static void check(Plugin plugin, int resourceId) {
Task.create(plugin)
.async()
.execute(() -> {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.spiget.org/v2/resources/%s/versions/latest".formatted(resourceId)))
.build();
plugin.getLogger().info("Checking for updates");
var tag = client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.thenApply(UpdateChecker::parseLatestTag)
.join();
if (tag.isEmpty()) {
plugin.getLogger().info("Failed to check for updates");
return;
}
var version = plugin.getDescription().getVersion();
if (!version.equals(tag)) {
plugin.getLogger().info("A new version is available: %s -> %s".formatted(version, tag));
plugin.getLogger().info("Download at https://spigotmc.org/resources/%s".formatted(resourceId));
} else {
plugin.getLogger().info("No new version found");
}
})
.run();
}
private static String parseLatestTag(String responseBody) {
Pattern pattern = Pattern.compile("\"name\": ?\"(.*?)\"");
Matcher matcher = pattern.matcher(responseBody);
if (matcher.find()) {
return matcher.group(1);
} else {
return "";
}
}
}

View file

@ -0,0 +1,87 @@
package dev.efnilite.vilib.util;
import org.bukkit.Bukkit;
import java.util.Arrays;
/**
* Version class, useful for checking whether certain features on legacy can be executed.
*
* @author Efnilite
*/
public enum Version {
V1_16(16), V1_17(17), V1_18(18), V1_19(19), V1_20(20), V1_20_5(20, 5),
V1_21(21), V1_22(22);
public static Version VERSION;
public final int major;
public final int minor;
Version(int major) {
this.major = major;
this.minor = 0;
}
Version(int major, int minor) {
this.major = major;
this.minor = minor;
}
/**
* Returns whether the version is higher or equal to a given version.
*
* @param compareTo The version to compare to
* @return true if the current version is higher or equal to the given version, false if not
*/
public static boolean isHigherOrEqual(Version compareTo) {
if (VERSION.major == compareTo.major) {
return VERSION.minor >= compareTo.minor;
} else {
return VERSION.major > compareTo.major;
}
}
/**
* Get the current version as a String which can be displayed to users.
*
* @return the pretty version as a String
*/
public static String getPrettyVersion() {
return Bukkit.getBukkitVersion().split("-")[0];
}
/**
* Returns the current version as an instance of this enum.
*
* @return the version.
*/
public static Version getVersion() {
var parts = getPrettyVersion().split("\\.");
var major = Integer.parseInt(parts[1]);
var minor = parts.length == 3 ? Integer.parseInt(parts[2]) : 0;
var lowerVersions = Arrays.stream(Version.values()).filter(version -> {
if (version.major == major) {
return version.minor <= minor;
} else {
return version.major < major;
}
}).toList();
VERSION = lowerVersions.get(lowerVersions.size() - 1);
return VERSION;
}
/**
* Gets the internal version from the Bukkit package.
* Format: "v1_20_R1"
*
* @return the internal version with format "v1_20_R1"
*/
@Deprecated(forRemoval = true)
public static String getInternalVersion() {
return Bukkit.getBukkitVersion().split("-")[0];
}
}

View file

@ -0,0 +1,107 @@
package dev.efnilite.vilib.util;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.block.Biome;
import org.bukkit.generator.BiomeProvider;
import org.bukkit.generator.ChunkGenerator;
import org.bukkit.generator.WorldInfo;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Random;
/**
* Wrapper class of the specific version void generators
* Inspired by the VoidGen plugin.
*/
public abstract class VoidGenerator extends ChunkGenerator {
@Override
public boolean shouldGenerateNoise() {
return false;
}
@Override
public boolean shouldGenerateSurface() {
return false;
}
@Override
public boolean shouldGenerateBedrock() {
return false;
}
@Override
public boolean shouldGenerateCaves() {
return false;
}
@Override
public boolean shouldGenerateDecorations() {
return false;
}
@Override
public boolean shouldGenerateMobs() {
return false;
}
@Override
public boolean shouldGenerateStructures() {
return false;
}
@NotNull
public static VoidGenerator getGenerator() {
if (Version.getVersion().major > 16) {
return new VoidGenerator_v1_17();
} else {
return new VoidGenerator_v1_16();
}
}
/**
* Returns the active void generator plugin
*
* @return the void generator currently in use
*/
public static @Nullable String getMultiverseGenerator() {
return Bukkit.getPluginManager().isPluginEnabled("VoidGen") ? "VoidGen" : null;
}
@SuppressWarnings("deprecation")
protected static class VoidGenerator_v1_16 extends VoidGenerator {
@NotNull
@Override
public ChunkData generateChunkData(@NotNull World world, @NotNull Random random, int x, int z, @NotNull ChunkGenerator.BiomeGrid biome) {
return createChunkData(world);
}
}
protected static class VoidGenerator_v1_17 extends VoidGenerator {
@Override
public BiomeProvider getDefaultBiomeProvider(@NotNull WorldInfo worldInfo) {
return new VoidGenerator_v1_17.VoidBiomeProvider();
}
private static class VoidBiomeProvider extends BiomeProvider {
@Override
@NotNull
public Biome getBiome(@NotNull WorldInfo worldInfo, int x, int y, int z) {
return Biome.PLAINS;
}
@Override
@NotNull
public List<Biome> getBiomes(@NotNull WorldInfo worldInfo) {
return List.of(Biome.PLAINS);
}
}
}
}