Upload via GUI (27 Dateien)
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
package de.landschunkinfo;
|
||||
|
||||
import me.angeschossen.lands.api.land.Land;
|
||||
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Platzhalter (Bezug: Hauptland des Spielers, also eigenes Land oder erstes Land mit Vertrauen):
|
||||
* %chunkinfo_land%, _used, _max, _free, _locked, _next_free, _upkeep, _upkeep_in,
|
||||
* %chunkinfo_top_<n>_name%, %chunkinfo_top_<n>_chunks%
|
||||
*/
|
||||
final class ChunkExpansion extends PlaceholderExpansion {
|
||||
|
||||
private final LandsChunkInfo plugin;
|
||||
|
||||
ChunkExpansion(LandsChunkInfo plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getIdentifier() {
|
||||
return "chunkinfo";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAuthor() {
|
||||
return "LandsChunkInfo";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVersion() {
|
||||
return plugin.getPluginMeta().getVersion();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean persist() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String onRequest(OfflinePlayer player, String params) {
|
||||
String key = params.toLowerCase(Locale.ROOT);
|
||||
if (key.startsWith("top_")) {
|
||||
String[] p = key.split("_");
|
||||
if (p.length != 3) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
int rank = Integer.parseInt(p[1]);
|
||||
List<Land> top = plugin.top(rank);
|
||||
if (top.size() < rank) {
|
||||
return "-";
|
||||
}
|
||||
Land l = top.get(rank - 1);
|
||||
return p[2].equals("name") ? l.getName() : p[2].equals("chunks") ? String.valueOf(l.getChunksAmount()) : null;
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (player == null) {
|
||||
return "";
|
||||
}
|
||||
Land land = plugin.primaryLand(player.getUniqueId());
|
||||
if (land == null) {
|
||||
return key.equals("land") ? "-" : "0";
|
||||
}
|
||||
List<Long> locked = plugin.cooldownActive() ? plugin.store.active(LandsChunkInfo.landId(land)) : List.of();
|
||||
long now = System.currentTimeMillis();
|
||||
return switch (key) {
|
||||
case "land" -> land.getName();
|
||||
case "used" -> String.valueOf(land.getChunksAmount());
|
||||
case "max" -> land.getMaxChunks() <= 0 ? "unbegrenzt" : String.valueOf(land.getMaxChunks());
|
||||
case "free" -> plugin.freeSlots(land) < 0 ? "unbegrenzt" : String.valueOf(plugin.freeSlots(land));
|
||||
case "locked" -> String.valueOf(locked.size());
|
||||
case "next_free" -> locked.isEmpty() ? "-" : TimeUtil.format(locked.get(0) - now);
|
||||
case "upkeep" -> String.format(Locale.GERMANY, "%.2f", land.getUpkeepCosts());
|
||||
case "upkeep_in" -> TimeUtil.format(plugin.nextUpkeep() - now);
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,482 @@
|
||||
package de.landschunkinfo;
|
||||
|
||||
import de.landschunkinfo.LockStore.Entry;
|
||||
import de.landschunkinfo.LockStore.Kind;
|
||||
import me.angeschossen.lands.api.framework.limit.Limit;
|
||||
import me.angeschossen.lands.api.land.Land;
|
||||
import me.angeschossen.lands.api.player.LandPlayer;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.TabCompleter;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
final class InfoCommand implements CommandExecutor, TabCompleter {
|
||||
|
||||
private static final List<String> SUBS = List.of("top", "skip", "gift", "queue", "watch", "temp", "reload");
|
||||
|
||||
private final LandsChunkInfo plugin;
|
||||
|
||||
InfoCommand(LandsChunkInfo plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ Dispatcher
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||
if (args.length >= 1 && args[0].equalsIgnoreCase("reload")) {
|
||||
if (!sender.hasPermission("landschunkinfo.admin")) {
|
||||
plugin.send(sender, plugin.cfgMsg("no-permission"));
|
||||
return true;
|
||||
}
|
||||
plugin.reload();
|
||||
plugin.send(sender, plugin.cfgMsg("reloaded"));
|
||||
return true;
|
||||
}
|
||||
if (!(sender instanceof Player p)) {
|
||||
plugin.send(sender, plugin.cfgMsg("players-only"));
|
||||
return true;
|
||||
}
|
||||
if (!p.hasPermission("landschunkinfo.use")) {
|
||||
plugin.send(p, plugin.cfgMsg("no-permission"));
|
||||
return true;
|
||||
}
|
||||
String sub = args.length == 0 ? "" : args[0].toLowerCase(Locale.ROOT);
|
||||
switch (sub) {
|
||||
case "top" -> top(p);
|
||||
case "skip" -> skip(p, args, false);
|
||||
case "gift" -> skip(p, args, true);
|
||||
case "queue" -> queue(p, args);
|
||||
case "watch" -> watch(p, args);
|
||||
case "temp" -> temp(p, args);
|
||||
default -> info(p, args);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean allowed(Player p, String perm) {
|
||||
if (p.hasPermission("landschunkinfo." + perm)) {
|
||||
return true;
|
||||
}
|
||||
plugin.send(p, plugin.cfgMsg("no-permission"));
|
||||
return false;
|
||||
}
|
||||
|
||||
private Land findLand(String name) {
|
||||
for (Object o : plugin.lands.getLands()) {
|
||||
Land l = (Land) o;
|
||||
if (l.getName().equalsIgnoreCase(name)) {
|
||||
return l;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean isMember(Land land, UUID uuid) {
|
||||
return uuid.equals(land.getOwnerUID()) || land.isTrusted(uuid);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ Info
|
||||
|
||||
private void info(Player p, String[] args) {
|
||||
List<Land> shown = plugin.landsOf(p.getUniqueId());
|
||||
if (args.length >= 1) {
|
||||
String wanted = String.join(" ", args);
|
||||
shown = shown.stream().filter(l -> l.getName().equalsIgnoreCase(wanted)).toList();
|
||||
if (shown.isEmpty()) {
|
||||
plugin.send(p, plugin.cfgMsg("land-not-found"));
|
||||
return;
|
||||
}
|
||||
} else if (shown.isEmpty()) {
|
||||
plugin.send(p, plugin.cfgMsg("no-land"));
|
||||
return;
|
||||
}
|
||||
|
||||
LandPlayer lp = plugin.lands.getLandPlayer(p.getUniqueId());
|
||||
if (lp != null) {
|
||||
Limit limit = Limit.getById("player_chunks");
|
||||
if (limit != null) {
|
||||
int lim = lp.getLimit(limit);
|
||||
p.sendMessage(plugin.line("&8&m &r &aDeine Chunks &8&m "));
|
||||
p.sendMessage(plugin.line("&7Von dir geclaimt: &e" + lp.getChunksClaimed().size()
|
||||
+ " &7/ &e" + (lim < 0 ? "unbegrenzt" : lim)));
|
||||
}
|
||||
}
|
||||
for (Land land : shown) {
|
||||
describe(p, land);
|
||||
}
|
||||
p.sendMessage(plugin.line("&8Mehr: &7/chunkinfo top | skip | gift | queue | watch | temp"));
|
||||
}
|
||||
|
||||
private void describe(Player p, Land land) {
|
||||
int max = land.getMaxChunks();
|
||||
int size = land.getChunksAmount();
|
||||
String id = LandsChunkInfo.landId(land);
|
||||
List<Long> locked = plugin.cooldownActive() ? plugin.store.active(id) : List.of();
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
p.sendMessage(plugin.line("&8&m &r &a" + land.getName() + " &8&m "));
|
||||
if (max <= 0) {
|
||||
p.sendMessage(plugin.line("&7Chunks: &e" + size + " &7(kein Limit)"));
|
||||
} else {
|
||||
p.sendMessage(plugin.line("&7Chunks: &e" + size + " &7/ &e" + max + " &8| &7Frei: &a"
|
||||
+ plugin.freeSlots(land) + (locked.isEmpty() ? "" : " &8| &7Gesperrt: &c" + locked.size())));
|
||||
}
|
||||
if (plugin.cooldownActive()) {
|
||||
if (locked.isEmpty()) {
|
||||
p.sendMessage(plugin.line("&7Gesperrte Slots: &akeine"));
|
||||
} else {
|
||||
long first = locked.get(0);
|
||||
p.sendMessage(plugin.line("&7Naechster Chunk wieder frei in: &e" + TimeUtil.format(first - now)
|
||||
+ " &8(" + LandsChunkInfo.fmt(first) + ")"));
|
||||
if (locked.size() > 1) {
|
||||
StringBuilder sb = new StringBuilder("&7Danach: ");
|
||||
locked.stream().skip(1).limit(4).forEach(t -> sb.append("&e")
|
||||
.append(TimeUtil.format(t - now)).append("&7, "));
|
||||
sb.setLength(sb.length() - 2);
|
||||
p.sendMessage(plugin.line(sb + (locked.size() > 5 ? " ..." : "")));
|
||||
}
|
||||
if (plugin.getConfig().getBoolean("skip.enabled", true)) {
|
||||
p.sendMessage(plugin.line("&7Sofort freischalten: &e/chunkinfo skip &7(Preis: &e"
|
||||
+ price(first - now) + "&7)"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (Entry t : plugin.store.entries(Kind.TEMP)) {
|
||||
if (t.landId().equals(id)) {
|
||||
p.sendMessage(plugin.line("&7Temp-Chunk &e" + t.x() + ", " + t.z() + " &7laeuft ab in &e"
|
||||
+ TimeUtil.format(t.until() - now)));
|
||||
}
|
||||
}
|
||||
|
||||
double upkeep = land.getUpkeepCosts();
|
||||
if (upkeep > 0) {
|
||||
long next = plugin.nextUpkeep();
|
||||
double balance = land.getBalance();
|
||||
p.sendMessage(plugin.line("&7Upkeep: &e" + money(upkeep) + " &7pro Zyklus, naechster Einzug in &e"
|
||||
+ TimeUtil.format(next - now) + " &8(" + LandsChunkInfo.fmt(next) + ")"));
|
||||
if (balance < upkeep) {
|
||||
String risk = "";
|
||||
YamlConfiguration lc = plugin.landsConfig();
|
||||
if (lc != null && lc.getBoolean("upkeep.unclaim", false) && lc.getDouble("upkeep.per-chunk", 0) > 0) {
|
||||
int lost = (int) Math.min(size, Math.ceil((upkeep - balance) / lc.getDouble("upkeep.per-chunk")));
|
||||
risk = " - " + lost + " Chunk(s) wuerden verloren gehen!";
|
||||
}
|
||||
p.sendMessage(plugin.line("&cGuthaben (" + money(balance) + ") reicht nicht fuer den Upkeep." + risk));
|
||||
}
|
||||
}
|
||||
|
||||
Long inactive = plugin.inactivityRemaining(land);
|
||||
if (inactive != null && inactive >= 0) {
|
||||
boolean ownerOnline = land.getOwnerUID() != null && Bukkit.getPlayer(land.getOwnerUID()) != null;
|
||||
p.sendMessage(plugin.line("&7Inaktivitaet des Besitzers: " + (ownerOnline
|
||||
? "&aBesitzer online"
|
||||
: "&eLand gilt in &c" + TimeUtil.format(inactive) + " &eals inaktiv")));
|
||||
}
|
||||
}
|
||||
|
||||
private static String money(double v) {
|
||||
return String.format(Locale.GERMANY, "%.2f", v);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------- Top
|
||||
|
||||
private void top(Player p) {
|
||||
int n = plugin.getConfig().getInt("top.size", 10);
|
||||
p.sendMessage(plugin.line("&8&m &r &aTop-Lands nach Chunks &8&m "));
|
||||
int i = 1;
|
||||
for (Land l : plugin.top(n)) {
|
||||
p.sendMessage(plugin.line("&e#" + i++ + " &7" + l.getName() + " &8- &a" + l.getChunksAmount() + " Chunks"));
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ Skip / Gift
|
||||
|
||||
private String price(long remainingMs) {
|
||||
return money(priceValue(remainingMs));
|
||||
}
|
||||
|
||||
private double priceValue(long remainingMs) {
|
||||
double perHour = plugin.getConfig().getDouble("skip.price-per-hour", 25.0);
|
||||
double min = plugin.getConfig().getDouble("skip.min-price", 50.0);
|
||||
return Math.max(min, remainingMs / 3_600_000.0 * perHour);
|
||||
}
|
||||
|
||||
/** /chunkinfo skip [Land] [confirm] bzw. /chunkinfo gift <Land> [confirm] */
|
||||
private void skip(Player p, String[] args, boolean gift) {
|
||||
if (!allowed(p, gift ? "gift" : "skip")) {
|
||||
return;
|
||||
}
|
||||
if (!plugin.getConfig().getBoolean("skip.enabled", true) || !plugin.cooldownActive()) {
|
||||
plugin.send(p, "&cDiese Funktion ist deaktiviert.");
|
||||
return;
|
||||
}
|
||||
boolean confirm = args.length > 1 && args[args.length - 1].equalsIgnoreCase("confirm");
|
||||
int end = confirm ? args.length - 1 : args.length;
|
||||
String name = end > 1 ? String.join(" ", List.of(args).subList(1, end)) : null;
|
||||
|
||||
Land land;
|
||||
if (name != null) {
|
||||
land = findLand(name);
|
||||
} else if (gift) {
|
||||
plugin.send(p, "&cNutzung: /chunkinfo gift <Land> [confirm]");
|
||||
return;
|
||||
} else {
|
||||
land = plugin.landsOf(p.getUniqueId()).stream()
|
||||
.filter(l -> !plugin.store.active(LandsChunkInfo.landId(l)).isEmpty()).findFirst().orElse(null);
|
||||
if (land == null) {
|
||||
plugin.send(p, "&aDu hast keine gesperrten Slots.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (land == null || (!gift && !isMember(land, p.getUniqueId()))) {
|
||||
plugin.send(p, plugin.cfgMsg("land-not-found"));
|
||||
return;
|
||||
}
|
||||
List<Long> locked = plugin.store.active(LandsChunkInfo.landId(land));
|
||||
if (locked.isEmpty()) {
|
||||
plugin.send(p, "&e" + land.getName() + " &ahat keine gesperrten Slots.");
|
||||
return;
|
||||
}
|
||||
long remaining = locked.get(0) - System.currentTimeMillis();
|
||||
double price = priceValue(remaining);
|
||||
boolean fromLand = !gift && "LAND".equalsIgnoreCase(plugin.getConfig().getString("skip.payment", "PLAYER"));
|
||||
|
||||
if (fromLand && !p.getUniqueId().equals(land.getOwnerUID())) {
|
||||
plugin.send(p, "&cNur der Besitzer darf aus der Landkasse zahlen.");
|
||||
return;
|
||||
}
|
||||
if (!fromLand && plugin.vault == null) {
|
||||
plugin.send(p, "&cDafuer wird Vault mit einem Economy-Plugin benoetigt.");
|
||||
return;
|
||||
}
|
||||
if (!confirm) {
|
||||
plugin.send(p, "&7Slot in &e" + land.getName() + " &7(noch &e" + TimeUtil.format(remaining)
|
||||
+ "&7) freischalten fuer &e" + money(price) + (fromLand ? " &7aus der Landkasse" : "")
|
||||
+ "&7. Bestaetigen: &e/chunkinfo " + (gift ? "gift " : "skip ") + land.getName() + " confirm");
|
||||
return;
|
||||
}
|
||||
|
||||
if (fromLand) {
|
||||
if (land.getBalance() < price) {
|
||||
plugin.send(p, "&cDie Landkasse hat nicht genug Guthaben.");
|
||||
return;
|
||||
}
|
||||
land.setBalance(land.getBalance() - price);
|
||||
} else {
|
||||
if (plugin.vault.balance(p) < price || !plugin.vault.withdraw(p, price)) {
|
||||
plugin.send(p, "&cDu hast nicht genug Geld.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!plugin.store.removeEarliest(LandsChunkInfo.landId(land))) {
|
||||
plugin.send(p, "&cDer Slot ist inzwischen von selbst frei geworden.");
|
||||
return;
|
||||
}
|
||||
plugin.send(p, "&aSlot in &e" + land.getName() + " &afreigeschaltet (&e" + money(price) + " &abezahlt).");
|
||||
if (gift) {
|
||||
for (Player m : plugin.onlineMembers(land)) {
|
||||
plugin.send(m, "&a" + p.getName() + " hat einen Chunk-Slot fuer &e" + land.getName() + " &afreigeschaltet!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------- Queue
|
||||
|
||||
private void queue(Player p, String[] args) {
|
||||
if (!allowed(p, "queue")) {
|
||||
return;
|
||||
}
|
||||
String arg = args.length > 1 ? args[1].toLowerCase(Locale.ROOT) : "";
|
||||
if (arg.equals("list")) {
|
||||
listEntries(p, Kind.QUEUE, "Warteschlange");
|
||||
return;
|
||||
}
|
||||
if (arg.equals("clear")) {
|
||||
plugin.send(p, "&7" + plugin.store.removeAll(Kind.QUEUE, p.getUniqueId()) + " Eintraege entfernt.");
|
||||
return;
|
||||
}
|
||||
Land land;
|
||||
if (args.length > 1) {
|
||||
land = findLand(String.join(" ", List.of(args).subList(1, args.length)));
|
||||
} else {
|
||||
land = plugin.primaryLand(p.getUniqueId());
|
||||
}
|
||||
if (land == null || !isMember(land, p.getUniqueId())) {
|
||||
plugin.send(p, plugin.cfgMsg("land-not-found"));
|
||||
return;
|
||||
}
|
||||
World w = p.getWorld();
|
||||
int cx = p.getLocation().getBlockX() >> 4;
|
||||
int cz = p.getLocation().getBlockZ() >> 4;
|
||||
if (plugin.lands.getLandByUnloadedChunk(w, cx, cz) != null) {
|
||||
plugin.send(p, "&cDieser Chunk ist schon geclaimt. Mit &e/chunkinfo watch &ckannst du ihn beobachten.");
|
||||
return;
|
||||
}
|
||||
List<Entry> mine = plugin.store.entries(Kind.QUEUE).stream().filter(e -> e.player().equals(p.getUniqueId())).toList();
|
||||
if (mine.size() >= plugin.getConfig().getInt("queue.max", 3)) {
|
||||
plugin.send(p, "&cDeine Warteschlange ist voll (&e/chunkinfo queue clear&c).");
|
||||
return;
|
||||
}
|
||||
if (mine.stream().anyMatch(e -> e.at(w.getName(), cx, cz))) {
|
||||
plugin.send(p, "&cDieser Chunk ist schon in deiner Warteschlange.");
|
||||
return;
|
||||
}
|
||||
plugin.store.add(Kind.QUEUE, new Entry(p.getUniqueId(), LandsChunkInfo.landId(land), w.getName(), cx, cz, 0));
|
||||
int free = plugin.freeSlots(land);
|
||||
plugin.send(p, "&aChunk &e" + cx + ", " + cz + " &avorgemerkt fuer &e" + land.getName() + "&a. "
|
||||
+ (free == 0 ? "Er wird geclaimt, sobald ein Slot frei wird (du musst dafuer online sein)."
|
||||
: "Er wird in Kuerze geclaimt."));
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------- Watch
|
||||
|
||||
private void watch(Player p, String[] args) {
|
||||
if (!allowed(p, "watch")) {
|
||||
return;
|
||||
}
|
||||
String arg = args.length > 1 ? args[1].toLowerCase(Locale.ROOT) : "";
|
||||
if (arg.equals("list")) {
|
||||
listEntries(p, Kind.WATCH, "Watchlist");
|
||||
return;
|
||||
}
|
||||
if (arg.equals("clear")) {
|
||||
plugin.send(p, "&7" + plugin.store.removeAll(Kind.WATCH, p.getUniqueId()) + " Eintraege entfernt.");
|
||||
return;
|
||||
}
|
||||
World w = p.getWorld();
|
||||
int cx = p.getLocation().getBlockX() >> 4;
|
||||
int cz = p.getLocation().getBlockZ() >> 4;
|
||||
Land holder = plugin.lands.getLandByUnloadedChunk(w, cx, cz);
|
||||
if (holder == null) {
|
||||
plugin.send(p, "&cDieser Chunk ist frei - du kannst ihn direkt claimen oder mit &e/chunkinfo queue &avormerken.");
|
||||
return;
|
||||
}
|
||||
List<Entry> mine = plugin.store.entries(Kind.WATCH).stream().filter(e -> e.player().equals(p.getUniqueId())).toList();
|
||||
if (mine.size() >= plugin.getConfig().getInt("watch.max", 5)) {
|
||||
plugin.send(p, "&cDeine Watchlist ist voll (&e/chunkinfo watch clear&c).");
|
||||
return;
|
||||
}
|
||||
if (mine.stream().anyMatch(e -> e.at(w.getName(), cx, cz))) {
|
||||
plugin.send(p, "&cDen beobachtest du schon.");
|
||||
return;
|
||||
}
|
||||
plugin.store.add(Kind.WATCH, new Entry(p.getUniqueId(), LandsChunkInfo.landId(holder), w.getName(), cx, cz, 0));
|
||||
plugin.send(p, "&aDu wirst benachrichtigt, sobald Chunk &e" + cx + ", " + cz + " &a(Land &e" + holder.getName() + "&a) frei wird.");
|
||||
}
|
||||
|
||||
private void listEntries(Player p, Kind kind, String title) {
|
||||
List<Entry> mine = plugin.store.entries(kind).stream().filter(e -> e.player().equals(p.getUniqueId())).toList();
|
||||
p.sendMessage(plugin.line("&8&m &r &a" + title + " &8&m "));
|
||||
if (mine.isEmpty()) {
|
||||
p.sendMessage(plugin.line("&7(leer)"));
|
||||
}
|
||||
for (Entry e : mine) {
|
||||
Land land = plugin.landById(e.landId());
|
||||
p.sendMessage(plugin.line("&e" + e.world() + " " + e.x() + ", " + e.z()
|
||||
+ " &8- &7Land: &e" + (land == null ? "?" : land.getName())));
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ Temp
|
||||
|
||||
private void temp(Player p, String[] args) {
|
||||
if (!allowed(p, "temp")) {
|
||||
return;
|
||||
}
|
||||
if (!plugin.getConfig().getBoolean("temp-claims.enabled", true)) {
|
||||
plugin.send(p, "&cDiese Funktion ist deaktiviert.");
|
||||
return;
|
||||
}
|
||||
World w = p.getWorld();
|
||||
int cx = p.getLocation().getBlockX() >> 4;
|
||||
int cz = p.getLocation().getBlockZ() >> 4;
|
||||
String arg = args.length > 1 ? args[1].toLowerCase(Locale.ROOT) : "";
|
||||
|
||||
if (arg.equals("list")) {
|
||||
p.sendMessage(plugin.line("&8&m &r &aTemp-Chunks &8&m "));
|
||||
long now = System.currentTimeMillis();
|
||||
plugin.store.entries(Kind.TEMP).stream().filter(e -> plugin.landsOf(p.getUniqueId()).stream()
|
||||
.anyMatch(l -> LandsChunkInfo.landId(l).equals(e.landId()))).forEach(e -> p.sendMessage(plugin.line(
|
||||
"&e" + e.world() + " " + e.x() + ", " + e.z() + " &8- &7noch &e" + TimeUtil.format(e.until() - now))));
|
||||
return;
|
||||
}
|
||||
Land land = plugin.lands.getLandByUnloadedChunk(w, cx, cz);
|
||||
if (land == null || !p.getUniqueId().equals(land.getOwnerUID())) {
|
||||
plugin.send(p, "&cDu musst in einem Chunk deines eigenen Lands stehen.");
|
||||
return;
|
||||
}
|
||||
String id = LandsChunkInfo.landId(land);
|
||||
if (arg.equals("cancel")) {
|
||||
boolean removed = plugin.store.entries(Kind.TEMP).stream()
|
||||
.filter(e -> e.landId().equals(id) && e.at(w.getName(), cx, cz))
|
||||
.anyMatch(e -> plugin.store.remove(Kind.TEMP, e));
|
||||
plugin.send(p, removed ? "&aDie Zeitbegrenzung fuer diesen Chunk wurde entfernt." : "&cDieser Chunk hat keine Zeitbegrenzung.");
|
||||
return;
|
||||
}
|
||||
int days;
|
||||
try {
|
||||
days = Integer.parseInt(arg);
|
||||
} catch (NumberFormatException ex) {
|
||||
plugin.send(p, "&cNutzung: /chunkinfo temp <Tage> | cancel | list");
|
||||
return;
|
||||
}
|
||||
int maxDays = plugin.getConfig().getInt("temp-claims.max-days", 30);
|
||||
if (days < 1 || days > maxDays) {
|
||||
plugin.send(p, "&cErlaubt sind 1 bis " + maxDays + " Tage.");
|
||||
return;
|
||||
}
|
||||
List<Entry> forLand = plugin.store.entries(Kind.TEMP).stream().filter(e -> e.landId().equals(id)).toList();
|
||||
Entry existing = forLand.stream().filter(e -> e.at(w.getName(), cx, cz)).findFirst().orElse(null);
|
||||
if (existing == null && forLand.size() >= plugin.getConfig().getInt("temp-claims.max-per-land", 3)) {
|
||||
plugin.send(p, "&cEs sind schon zu viele Temp-Chunks in diesem Land.");
|
||||
return;
|
||||
}
|
||||
if (existing != null) {
|
||||
plugin.store.remove(Kind.TEMP, existing);
|
||||
}
|
||||
long until = System.currentTimeMillis() + days * 86_400_000L;
|
||||
plugin.store.add(Kind.TEMP, new Entry(p.getUniqueId(), id, w.getName(), cx, cz, until));
|
||||
plugin.send(p, "&aChunk &e" + cx + ", " + cz + " &awird in &e" + days + " Tag(en) &aautomatisch freigegeben ("
|
||||
+ LandsChunkInfo.fmt(until) + ").");
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------- Tab
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(CommandSender s, Command c, String alias, String[] args) {
|
||||
if (!(s instanceof Player p)) {
|
||||
return List.of();
|
||||
}
|
||||
String last = args[args.length - 1].toLowerCase(Locale.ROOT);
|
||||
List<String> out = new ArrayList<>();
|
||||
if (args.length == 1) {
|
||||
out.addAll(SUBS);
|
||||
plugin.landsOf(p.getUniqueId()).forEach(l -> out.add(l.getName()));
|
||||
} else {
|
||||
switch (args[0].toLowerCase(Locale.ROOT)) {
|
||||
case "queue", "watch" -> out.addAll(args.length == 2 ? List.of("list", "clear") : List.of());
|
||||
case "temp" -> out.addAll(List.of("list", "cancel", "1", "7", "14", "30"));
|
||||
case "skip", "gift" -> {
|
||||
out.add("confirm");
|
||||
plugin.landsOf(p.getUniqueId()).forEach(l -> out.add(l.getName()));
|
||||
}
|
||||
default -> { }
|
||||
}
|
||||
if (args[0].equalsIgnoreCase("queue")) {
|
||||
plugin.landsOf(p.getUniqueId()).forEach(l -> out.add(l.getName()));
|
||||
}
|
||||
}
|
||||
return out.stream().filter(o -> o.toLowerCase(Locale.ROOT).startsWith(last)).toList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,513 @@
|
||||
package de.landschunkinfo;
|
||||
|
||||
import de.landschunkinfo.LockStore.Entry;
|
||||
import de.landschunkinfo.LockStore.Kind;
|
||||
import me.angeschossen.lands.api.LandsIntegration;
|
||||
import me.angeschossen.lands.api.applicationframework.util.ULID;
|
||||
import me.angeschossen.lands.api.events.ChunkDeleteEvent;
|
||||
import me.angeschossen.lands.api.events.ChunkPreClaimEvent;
|
||||
import me.angeschossen.lands.api.events.land.DeleteReason;
|
||||
import me.angeschossen.lands.api.events.memberholder.MemberHolderUpkeepEvent;
|
||||
import me.angeschossen.lands.api.land.Land;
|
||||
import me.angeschossen.lands.api.player.LandPlayer;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.Statistic;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import java.io.File;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class LandsChunkInfo extends JavaPlugin implements Listener {
|
||||
|
||||
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd.MM. HH:mm");
|
||||
private static final long TICK_SECONDS = 30;
|
||||
|
||||
LandsIntegration lands;
|
||||
LockStore store;
|
||||
VaultHook vault;
|
||||
|
||||
private long cooldownMillis;
|
||||
private long lastTick = System.currentTimeMillis();
|
||||
/** Unclaims, die vom Plugin selbst ausgeloest wurden (Temp-Claims) und keinen Slot sperren sollen. */
|
||||
private final Set<String> silent = ConcurrentHashMap.newKeySet();
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
saveDefaultConfig();
|
||||
lands = LandsIntegration.of(this);
|
||||
store = new LockStore(getDataFolder(), getLogger());
|
||||
reload();
|
||||
|
||||
if (Bukkit.getPluginManager().getPlugin("Vault") != null) {
|
||||
vault = VaultHook.create();
|
||||
}
|
||||
if (vault == null) {
|
||||
getLogger().info("Vault/Economy nicht gefunden: Bezahlung nur aus der Landkasse moeglich, /chunkinfo gift ist deaktiviert.");
|
||||
}
|
||||
if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) {
|
||||
new ChunkExpansion(this).register();
|
||||
}
|
||||
|
||||
Bukkit.getPluginManager().registerEvents(this, this);
|
||||
InfoCommand command = new InfoCommand(this);
|
||||
var cmd = getCommand("chunkinfo");
|
||||
if (cmd != null) {
|
||||
cmd.setExecutor(command);
|
||||
cmd.setTabCompleter(command);
|
||||
}
|
||||
Bukkit.getScheduler().runTaskTimer(this, this::tick, 100L, TICK_SECONDS * 20L);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
if (store != null) {
|
||||
store.save();
|
||||
}
|
||||
}
|
||||
|
||||
void reload() {
|
||||
reloadConfig();
|
||||
cooldownMillis = TimeUtil.parse(getConfig().getString("unclaim-cooldown.duration", "24h"));
|
||||
if (cooldownMillis < 0) {
|
||||
getLogger().warning("unclaim-cooldown.duration ist ungueltig, Cooldown wird deaktiviert.");
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- Helfer
|
||||
|
||||
boolean cooldownActive() {
|
||||
return getConfig().getBoolean("unclaim-cooldown.enabled", true) && cooldownMillis > 0;
|
||||
}
|
||||
|
||||
long cooldownMillis() {
|
||||
return cooldownMillis;
|
||||
}
|
||||
|
||||
Component line(String text) {
|
||||
return LegacyComponentSerializer.legacyAmpersand().deserialize(text);
|
||||
}
|
||||
|
||||
void send(org.bukkit.command.CommandSender to, String text) {
|
||||
to.sendMessage(line(getConfig().getString("messages.prefix", "") + text));
|
||||
}
|
||||
|
||||
String cfgMsg(String key, String... repl) {
|
||||
String text = getConfig().getString("messages." + key, key);
|
||||
for (int i = 0; i + 1 < repl.length; i += 2) {
|
||||
text = text.replace(repl[i], repl[i + 1]);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
static String landId(Land land) {
|
||||
return land.getULID().toString();
|
||||
}
|
||||
|
||||
Land landById(String id) {
|
||||
try {
|
||||
return lands.getLandByULID(ULID.fromString(id));
|
||||
} catch (RuntimeException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Lands, in denen die Person Besitzer oder vertraut ist. */
|
||||
List<Land> landsOf(UUID uuid) {
|
||||
List<Land> out = new ArrayList<>();
|
||||
for (Object o : lands.getLands()) {
|
||||
Land l = (Land) o;
|
||||
if (uuid.equals(l.getOwnerUID()) || l.isTrusted(uuid)) {
|
||||
out.add(l);
|
||||
}
|
||||
}
|
||||
out.sort(Comparator.comparing((Land l) -> !uuid.equals(l.getOwnerUID())).thenComparing(Land::getName));
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Hauptland: eigenes Land, sonst das erste Land, in dem man vertraut ist. */
|
||||
Land primaryLand(UUID uuid) {
|
||||
List<Land> l = landsOf(uuid);
|
||||
return l.isEmpty() ? null : l.get(0);
|
||||
}
|
||||
|
||||
/** Freie Slots; -1 = kein Limit. */
|
||||
int freeSlots(Land land) {
|
||||
int max = land.getMaxChunks();
|
||||
if (max <= 0) {
|
||||
return -1;
|
||||
}
|
||||
int locked = cooldownActive() ? store.active(landId(land)).size() : 0;
|
||||
return Math.max(0, max - land.getChunksAmount() - locked);
|
||||
}
|
||||
|
||||
List<Player> onlineMembers(Land land) {
|
||||
List<Player> out = new ArrayList<>();
|
||||
for (Object o : land.getOnlinePlayers()) {
|
||||
if (o instanceof Player p) {
|
||||
out.add(p);
|
||||
} else if (o instanceof LandPlayer lp && lp.getPlayer() != null) {
|
||||
out.add(lp.getPlayer());
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Land-Rangliste nach Chunks, absteigend. */
|
||||
List<Land> top(int n) {
|
||||
List<Land> all = new ArrayList<>();
|
||||
for (Object o : lands.getLands()) {
|
||||
all.add((Land) o);
|
||||
}
|
||||
all.sort(Comparator.comparingInt(Land::getChunksAmount).reversed());
|
||||
return all.subList(0, Math.min(n, all.size()));
|
||||
}
|
||||
|
||||
static String fmt(long millis) {
|
||||
return DATE.format(LocalDateTime.ofInstant(Instant.ofEpochMilli(millis), ZoneId.systemDefault()));
|
||||
}
|
||||
|
||||
private static String chunkKey(String landId, String world, int x, int z) {
|
||||
return landId + "|" + world + "|" + x + "|" + z;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- Events
|
||||
|
||||
/** Manuelles Unclaim sperrt den Slot fuer die konfigurierte Zeit. */
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onUnclaim(ChunkDeleteEvent e) {
|
||||
String id = landId(e.getLand());
|
||||
if (silent.remove(chunkKey(id, e.getWorld().getName(), e.getX(), e.getZ()))) {
|
||||
return;
|
||||
}
|
||||
if (!cooldownActive() || e.getReason() != DeleteReason.DEFAULT
|
||||
|| "LAND_DELETION".equals(String.valueOf(e.getUnclaimType()))) {
|
||||
return;
|
||||
}
|
||||
LandPlayer lp = e.getLandPlayer();
|
||||
Player p = lp == null ? null : lp.getPlayer();
|
||||
if (p != null && p.hasPermission("landschunkinfo.bypass")) {
|
||||
return;
|
||||
}
|
||||
store.add(id, System.currentTimeMillis() + cooldownMillis);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
|
||||
public void onClaim(ChunkPreClaimEvent e) {
|
||||
if (!cooldownActive() || !getConfig().getBoolean("unclaim-cooldown.enforce", true)) {
|
||||
return;
|
||||
}
|
||||
LandPlayer lp = e.getLandPlayer();
|
||||
Player p = lp == null ? null : lp.getPlayer();
|
||||
if (p != null && p.hasPermission("landschunkinfo.bypass")) {
|
||||
return;
|
||||
}
|
||||
Land land = e.getLand();
|
||||
int max = land.getMaxChunks();
|
||||
if (max <= 0) {
|
||||
return;
|
||||
}
|
||||
int size = land.getChunksAmount();
|
||||
List<Long> locked = store.active(landId(land));
|
||||
// Nur eingreifen, wenn der Platz allein wegen der Sperren fehlt; sonst uebernimmt Lands.
|
||||
if (size < max && size + locked.size() >= max) {
|
||||
e.setCancelled(true);
|
||||
if (p != null) {
|
||||
long next = locked.get(Math.max(0, size + locked.size() - max)) - System.currentTimeMillis();
|
||||
send(p, cfgMsg("claim-blocked", "%time%", TimeUtil.format(next)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onUpkeep(MemberHolderUpkeepEvent e) {
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - store.lastUpkeep() > 600_000L) {
|
||||
store.lastUpkeep(now);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onJoin(PlayerJoinEvent e) {
|
||||
if (!getConfig().getBoolean("join-reminder.enabled", true)) {
|
||||
return;
|
||||
}
|
||||
Player p = e.getPlayer();
|
||||
Bukkit.getScheduler().runTaskLater(this, () -> joinSummary(p), 60L);
|
||||
}
|
||||
|
||||
private void joinSummary(Player p) {
|
||||
if (!p.isOnline() || !p.hasPermission("landschunkinfo.use")) {
|
||||
return;
|
||||
}
|
||||
long now = System.currentTimeMillis();
|
||||
long warnMs = (long) (getConfig().getDouble("join-reminder.upkeep-warning-hours", 48) * 3_600_000L);
|
||||
for (Land land : landsOf(p.getUniqueId())) {
|
||||
int free = freeSlots(land);
|
||||
if (free > 0 && land.getMaxChunks() > 0) {
|
||||
send(p, "&aIn &e" + land.getName() + " &asind &e" + free + " &afreie Chunk-Slots verfuegbar.");
|
||||
}
|
||||
List<Long> locked = cooldownActive() ? store.active(landId(land)) : List.of();
|
||||
if (!locked.isEmpty()) {
|
||||
send(p, "&7" + land.getName() + ": naechster Slot frei in &e" + TimeUtil.format(locked.get(0) - now));
|
||||
}
|
||||
double upkeep = land.getUpkeepCosts();
|
||||
if (upkeep > 0 && land.getBalance() < upkeep && nextUpkeep() - now <= warnMs
|
||||
&& p.getUniqueId().equals(land.getOwnerUID())) {
|
||||
send(p, "&cAchtung: &e" + land.getName() + " &chat zu wenig Guthaben fuer den Upkeep (Einzug in &e"
|
||||
+ TimeUtil.format(nextUpkeep() - now) + "&c).");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- Ticker
|
||||
|
||||
private void tick() {
|
||||
long now = System.currentTimeMillis();
|
||||
try {
|
||||
notifyExpiredLocks(now);
|
||||
processTemp(now);
|
||||
processQueue();
|
||||
processWatch();
|
||||
} catch (RuntimeException ex) {
|
||||
getLogger().warning("Fehler im Hintergrund-Task: " + ex);
|
||||
}
|
||||
lastTick = now;
|
||||
}
|
||||
|
||||
private void notifyExpiredLocks(long now) {
|
||||
for (String id : store.lockedLands()) {
|
||||
Land land = landById(id);
|
||||
if (land == null) {
|
||||
store.clearLocks(id);
|
||||
continue;
|
||||
}
|
||||
long expired = store.all(id).stream().filter(t -> t > lastTick && t <= now).count();
|
||||
if (expired > 0) {
|
||||
for (Player p : onlineMembers(land)) {
|
||||
send(p, "&aIn &e" + land.getName() + " &aist wieder &e" + expired + " &aChunk-Slot frei!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processQueue() {
|
||||
Set<String> usedThisTick = new HashSet<>();
|
||||
for (Entry e : store.entries(Kind.QUEUE)) {
|
||||
Player p = Bukkit.getPlayer(e.player());
|
||||
Land land = landById(e.landId());
|
||||
World w = Bukkit.getWorld(e.world());
|
||||
if (land == null || w == null) {
|
||||
store.remove(Kind.QUEUE, e);
|
||||
continue;
|
||||
}
|
||||
if (p == null) {
|
||||
continue;
|
||||
}
|
||||
Land holder = lands.getLandByUnloadedChunk(w, e.x(), e.z());
|
||||
if (holder != null) {
|
||||
if (holder.equals(land)) {
|
||||
store.remove(Kind.QUEUE, e);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (freeSlots(land) == 0 || !usedThisTick.add(e.landId())) {
|
||||
continue;
|
||||
}
|
||||
LandPlayer lp = lands.getLandPlayer(e.player());
|
||||
if (lp == null) {
|
||||
continue;
|
||||
}
|
||||
store.remove(Kind.QUEUE, e);
|
||||
java.util.concurrent.CompletableFuture<?> f = land.claimChunk(lp, w, e.x(), e.z());
|
||||
f.whenComplete((r, ex) -> Bukkit.getScheduler().runTask(this, () -> {
|
||||
if (!p.isOnline()) {
|
||||
return;
|
||||
}
|
||||
String where = e.x() + ", " + e.z();
|
||||
if (ex == null && land.hasChunk(w, e.x(), e.z())) {
|
||||
send(p, "&aWarteschlange: Chunk &e" + where + " &awurde fuer &e" + land.getName() + " &ageclaimt.");
|
||||
} else {
|
||||
send(p, "&cWarteschlange: Chunk &e" + where + " &ckonnte nicht geclaimt werden (Eintrag entfernt).");
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
private void processWatch() {
|
||||
for (Entry e : store.entries(Kind.WATCH)) {
|
||||
World w = Bukkit.getWorld(e.world());
|
||||
if (w == null) {
|
||||
store.remove(Kind.WATCH, e);
|
||||
continue;
|
||||
}
|
||||
Player p = Bukkit.getPlayer(e.player());
|
||||
if (p == null) {
|
||||
continue;
|
||||
}
|
||||
if (lands.getLandByUnloadedChunk(w, e.x(), e.z()) == null) {
|
||||
store.remove(Kind.WATCH, e);
|
||||
send(p, "&aDer beobachtete Chunk &e" + e.x() + ", " + e.z() + " &a(" + e.world()
|
||||
+ ") ist jetzt frei! Mit &e/chunkinfo queue &akannst du ihn dir sichern.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processTemp(long now) {
|
||||
for (Entry e : store.entries(Kind.TEMP)) {
|
||||
if (e.until() > now) {
|
||||
continue;
|
||||
}
|
||||
Land land = landById(e.landId());
|
||||
World w = Bukkit.getWorld(e.world());
|
||||
if (land == null || w == null || !land.hasChunk(w, e.x(), e.z())) {
|
||||
store.remove(Kind.TEMP, e);
|
||||
continue;
|
||||
}
|
||||
LandPlayer lp = lands.getLandPlayer(e.player());
|
||||
if (lp == null && land.getOwnerUID() != null) {
|
||||
lp = lands.getLandPlayer(land.getOwnerUID());
|
||||
}
|
||||
if (lp == null) {
|
||||
continue; // wartet, bis ein Verantwortlicher online ist
|
||||
}
|
||||
store.remove(Kind.TEMP, e);
|
||||
String key = chunkKey(e.landId(), e.world(), e.x(), e.z());
|
||||
silent.add(key);
|
||||
Bukkit.getScheduler().runTaskLater(this, () -> silent.remove(key), 200L);
|
||||
land.unclaimChunk(w, e.x(), e.z(), lp);
|
||||
for (Player p : onlineMembers(land)) {
|
||||
send(p, "&7Der zeitlich begrenzte Chunk &e" + e.x() + ", " + e.z() + " &7von &e" + land.getName()
|
||||
+ " &7ist abgelaufen und wurde freigegeben.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- Upkeep
|
||||
|
||||
YamlConfiguration landsConfig() {
|
||||
Plugin pl = Bukkit.getPluginManager().getPlugin("Lands");
|
||||
File f = pl == null ? null : new File(pl.getDataFolder(), "config.yml");
|
||||
return f != null && f.exists() ? YamlConfiguration.loadConfiguration(f) : null;
|
||||
}
|
||||
|
||||
/** Naechster Upkeep-Einzug in epoch-millis, abgeleitet aus Lands economic.interval. */
|
||||
long nextUpkeep() {
|
||||
ZoneId zone = ZoneId.systemDefault();
|
||||
int days = 1;
|
||||
LocalTime time = LocalTime.MIDNIGHT;
|
||||
YamlConfiguration lc = landsConfig();
|
||||
if (lc != null) {
|
||||
days = Math.max(1, lc.getInt("economic.interval.days_2", lc.getInt("economic.interval.days", 1)));
|
||||
try {
|
||||
time = LocalTime.parse(lc.getString("economic.interval.time", "00:00:00"));
|
||||
} catch (Exception ignored) {
|
||||
// Standardwert behalten
|
||||
}
|
||||
}
|
||||
LocalDate anchor = null;
|
||||
if (store.lastUpkeep() > 0) {
|
||||
anchor = Instant.ofEpochMilli(store.lastUpkeep()).atZone(zone).toLocalDate();
|
||||
} else {
|
||||
try {
|
||||
String cfg = getConfig().getString("upkeep.anchor-date", "");
|
||||
if (!cfg.isBlank()) {
|
||||
anchor = LocalDate.parse(cfg);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// ungueltiges Datum -> Schaetzung
|
||||
}
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now(zone);
|
||||
if (anchor == null) {
|
||||
LocalDateTime next = now.toLocalDate().atTime(time);
|
||||
return (next.isAfter(now) ? next : next.plusDays(1)).atZone(zone).toInstant().toEpochMilli();
|
||||
}
|
||||
LocalDateTime next = anchor.atTime(time);
|
||||
while (!next.isAfter(now)) {
|
||||
next = next.plusDays(days);
|
||||
}
|
||||
return next.atZone(zone).toInstant().toEpochMilli();
|
||||
}
|
||||
|
||||
// --------------------------------------------------------- Inaktivitaet
|
||||
|
||||
/**
|
||||
* Restzeit bis der Besitzer laut Lands-Einstellung (expiration.owner_list) als inaktiv gilt.
|
||||
* Gibt null zurueck, wenn die Funktion in Lands nicht aktiv ist, -1 bei "never".
|
||||
*/
|
||||
Long inactivityRemaining(Land land) {
|
||||
YamlConfiguration lc = landsConfig();
|
||||
UUID owner = land.getOwnerUID();
|
||||
if (lc == null || owner == null) {
|
||||
return null;
|
||||
}
|
||||
List<String> list = null;
|
||||
for (String key : lc.getKeys(true)) {
|
||||
if (key.endsWith("expiration.owner_list")) {
|
||||
list = lc.getStringList(key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (list == null || list.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
OfflinePlayer op = Bukkit.getOfflinePlayer(owner);
|
||||
long playMs = op.getStatistic(Statistic.PLAY_ONE_MINUTE) * 50L;
|
||||
String chosen = null;
|
||||
long chosenPlay = Long.MAX_VALUE;
|
||||
String highest = null;
|
||||
long highestPlay = -1;
|
||||
for (String entry : list) {
|
||||
String[] parts = entry.split(":");
|
||||
long pt = parts.length == 2 ? TimeUtil.parse(parts[0].trim()) : -1;
|
||||
if (pt < 0) {
|
||||
continue;
|
||||
}
|
||||
if (pt >= playMs && pt < chosenPlay) {
|
||||
chosen = parts[1].trim();
|
||||
chosenPlay = pt;
|
||||
}
|
||||
if (pt > highestPlay) {
|
||||
highest = parts[1].trim();
|
||||
highestPlay = pt;
|
||||
}
|
||||
}
|
||||
String offline = chosen != null ? chosen : highest;
|
||||
if (offline == null) {
|
||||
return null;
|
||||
}
|
||||
if (offline.equalsIgnoreCase("never")) {
|
||||
return -1L;
|
||||
}
|
||||
long threshold = TimeUtil.parse(offline);
|
||||
if (threshold < 0) {
|
||||
return null;
|
||||
}
|
||||
long lastSeen = op.isOnline() ? System.currentTimeMillis() : op.getLastSeen();
|
||||
return Math.max(0, threshold - (System.currentTimeMillis() - lastSeen));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package de.landschunkinfo;
|
||||
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/** Persistiert Slot-Sperren, Queue, Watchlist, Temp-Claims und den letzten Upkeep-Einzug. */
|
||||
final class LockStore {
|
||||
|
||||
enum Kind { QUEUE, WATCH, TEMP }
|
||||
|
||||
/** Eintrag fuer Queue/Watch/Temp. {@code until} wird nur bei TEMP genutzt. */
|
||||
record Entry(UUID player, String landId, String world, int x, int z, long until) {
|
||||
|
||||
String serialize() {
|
||||
return player + ";" + landId + ";" + world + ";" + x + ";" + z + ";" + until;
|
||||
}
|
||||
|
||||
static Entry parse(String s) {
|
||||
String[] p = s.split(";");
|
||||
return new Entry(UUID.fromString(p[0]), p[1], p[2], Integer.parseInt(p[3]), Integer.parseInt(p[4]),
|
||||
Long.parseLong(p[5]));
|
||||
}
|
||||
|
||||
boolean at(String world, int x, int z) {
|
||||
return this.world.equals(world) && this.x == x && this.z == z;
|
||||
}
|
||||
}
|
||||
|
||||
private final File file;
|
||||
private final Logger log;
|
||||
private final Map<String, List<Long>> locks = new HashMap<>();
|
||||
private final Map<Kind, List<Entry>> entries = new EnumMap<>(Kind.class);
|
||||
private long lastUpkeep;
|
||||
|
||||
LockStore(File dataFolder, Logger log) {
|
||||
this.file = new File(dataFolder, "data.yml");
|
||||
this.log = log;
|
||||
for (Kind k : Kind.values()) {
|
||||
entries.put(k, new ArrayList<>());
|
||||
}
|
||||
load();
|
||||
}
|
||||
|
||||
private void load() {
|
||||
if (!file.exists()) {
|
||||
return;
|
||||
}
|
||||
YamlConfiguration yml = YamlConfiguration.loadConfiguration(file);
|
||||
lastUpkeep = yml.getLong("last-upkeep", 0);
|
||||
var section = yml.getConfigurationSection("locks");
|
||||
if (section != null) {
|
||||
for (String land : section.getKeys(false)) {
|
||||
locks.put(land, new ArrayList<>(section.getLongList(land)));
|
||||
}
|
||||
}
|
||||
for (Kind k : Kind.values()) {
|
||||
for (String s : yml.getStringList(k.name().toLowerCase())) {
|
||||
try {
|
||||
entries.get(k).add(Entry.parse(s));
|
||||
} catch (RuntimeException e) {
|
||||
log.warning("Ungueltiger Eintrag in data.yml uebersprungen: " + s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
synchronized void save() {
|
||||
YamlConfiguration yml = new YamlConfiguration();
|
||||
yml.set("last-upkeep", lastUpkeep);
|
||||
purge();
|
||||
locks.forEach((land, list) -> yml.set("locks." + land, list));
|
||||
for (Kind k : Kind.values()) {
|
||||
yml.set(k.name().toLowerCase(), entries.get(k).stream().map(Entry::serialize).toList());
|
||||
}
|
||||
try {
|
||||
yml.save(file);
|
||||
} catch (IOException e) {
|
||||
log.warning("data.yml konnte nicht gespeichert werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void purge() {
|
||||
long now = System.currentTimeMillis();
|
||||
locks.values().forEach(l -> l.removeIf(t -> t <= now));
|
||||
locks.values().removeIf(List::isEmpty);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ Sperren
|
||||
|
||||
synchronized void add(String landId, long until) {
|
||||
locks.computeIfAbsent(landId, k -> new ArrayList<>()).add(until);
|
||||
save();
|
||||
}
|
||||
|
||||
/** Aktive Sperren eines Lands, aufsteigend nach Ablaufzeit. */
|
||||
synchronized List<Long> active(String landId) {
|
||||
long now = System.currentTimeMillis();
|
||||
List<Long> list = locks.get(landId);
|
||||
if (list == null) {
|
||||
return List.of();
|
||||
}
|
||||
return list.stream().filter(t -> t > now).sorted().toList();
|
||||
}
|
||||
|
||||
/** Alle Land-IDs, die (evtl. abgelaufene) Sperren halten. */
|
||||
synchronized List<String> lockedLands() {
|
||||
return new ArrayList<>(locks.keySet());
|
||||
}
|
||||
|
||||
/** Alle Sperren eines Lands inkl. abgelaufener, fuer Ablauf-Benachrichtigungen. */
|
||||
synchronized List<Long> all(String landId) {
|
||||
return new ArrayList<>(locks.getOrDefault(landId, List.of()));
|
||||
}
|
||||
|
||||
/** Entfernt die fruehste aktive Sperre. */
|
||||
synchronized boolean removeEarliest(String landId) {
|
||||
List<Long> list = locks.get(landId);
|
||||
if (list == null) {
|
||||
return false;
|
||||
}
|
||||
long now = System.currentTimeMillis();
|
||||
Long min = list.stream().filter(t -> t > now).min(Long::compare).orElse(null);
|
||||
if (min == null) {
|
||||
return false;
|
||||
}
|
||||
list.remove(min);
|
||||
save();
|
||||
return true;
|
||||
}
|
||||
|
||||
synchronized void clearLocks(String landId) {
|
||||
if (locks.remove(landId) != null) {
|
||||
save();
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ Eintraege
|
||||
|
||||
synchronized List<Entry> entries(Kind kind) {
|
||||
return new ArrayList<>(entries.get(kind));
|
||||
}
|
||||
|
||||
synchronized void add(Kind kind, Entry entry) {
|
||||
entries.get(kind).add(entry);
|
||||
save();
|
||||
}
|
||||
|
||||
synchronized boolean remove(Kind kind, Entry entry) {
|
||||
boolean removed = entries.get(kind).remove(entry);
|
||||
if (removed) {
|
||||
save();
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
synchronized int removeAll(Kind kind, UUID player) {
|
||||
int before = entries.get(kind).size();
|
||||
entries.get(kind).removeIf(e -> e.player().equals(player));
|
||||
int removed = before - entries.get(kind).size();
|
||||
if (removed > 0) {
|
||||
save();
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ Upkeep
|
||||
|
||||
synchronized long lastUpkeep() {
|
||||
return lastUpkeep;
|
||||
}
|
||||
|
||||
synchronized void lastUpkeep(long time) {
|
||||
this.lastUpkeep = time;
|
||||
save();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package de.landschunkinfo;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
final class TimeUtil {
|
||||
|
||||
private static final Pattern PART = Pattern.compile("(\\d+)\\s*([smhd])", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private TimeUtil() {
|
||||
}
|
||||
|
||||
/** Parses values like "30s", "15m", "12h", "2d" or "1d12h" to milliseconds. Returns -1 if invalid. */
|
||||
static long parse(String input) {
|
||||
if (input == null || input.isBlank()) {
|
||||
return -1;
|
||||
}
|
||||
Matcher m = PART.matcher(input);
|
||||
long total = 0;
|
||||
int matched = 0;
|
||||
while (m.find()) {
|
||||
long n = Long.parseLong(m.group(1));
|
||||
total += n * switch (Character.toLowerCase(m.group(2).charAt(0))) {
|
||||
case 's' -> 1_000L;
|
||||
case 'm' -> 60_000L;
|
||||
case 'h' -> 3_600_000L;
|
||||
default -> 86_400_000L;
|
||||
};
|
||||
matched += m.group().length();
|
||||
}
|
||||
return matched == input.replace(" ", "").length() && total > 0 ? total : -1;
|
||||
}
|
||||
|
||||
static String format(long millis) {
|
||||
long s = Math.max(0, millis) / 1000;
|
||||
long d = s / 86400;
|
||||
long h = (s % 86400) / 3600;
|
||||
long m = (s % 3600) / 60;
|
||||
if (d > 0) {
|
||||
return d + "T " + h + "Std " + m + "Min";
|
||||
}
|
||||
if (h > 0) {
|
||||
return h + "Std " + m + "Min";
|
||||
}
|
||||
if (m > 0) {
|
||||
return m + "Min " + (s % 60) + "Sek";
|
||||
}
|
||||
return s + "Sek";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package de.landschunkinfo;
|
||||
|
||||
import net.milkbowl.vault.economy.Economy;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.plugin.RegisteredServiceProvider;
|
||||
|
||||
/** Duenne Huelle um Vault. Wird nur geladen, wenn Vault installiert ist. */
|
||||
final class VaultHook {
|
||||
|
||||
private final Economy economy;
|
||||
|
||||
private VaultHook(Economy economy) {
|
||||
this.economy = economy;
|
||||
}
|
||||
|
||||
static VaultHook create() {
|
||||
RegisteredServiceProvider<Economy> rsp = Bukkit.getServicesManager().getRegistration(Economy.class);
|
||||
return rsp == null ? null : new VaultHook(rsp.getProvider());
|
||||
}
|
||||
|
||||
double balance(OfflinePlayer p) {
|
||||
return economy.getBalance(p);
|
||||
}
|
||||
|
||||
boolean withdraw(OfflinePlayer p, double amount) {
|
||||
return economy.withdrawPlayer(p, amount).transactionSuccess();
|
||||
}
|
||||
|
||||
String format(double amount) {
|
||||
return economy.format(amount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
# Unclaim-Cooldown: Nach einem manuellen /unclaim bleibt der Chunk-Slot des Lands
|
||||
# für diese Zeit gesperrt. Format: 30s, 15m, 12h, 2d oder kombiniert (1d12h).
|
||||
unclaim-cooldown:
|
||||
enabled: true
|
||||
duration: 24h
|
||||
# true = Claimen wird blockiert, solange alle Slots belegt oder gesperrt sind.
|
||||
# false = nur Anzeige, kein Eingriff ins Claimen.
|
||||
enforce: true
|
||||
|
||||
# Upkeep-Anzeige. Zeitpunkt wird aus der Lands-config.yml (economic.interval) gelesen.
|
||||
# Der letzte echte Einzug wird automatisch gespeichert. Bis dahin kann ein Startdatum
|
||||
# (Tag eines bekannten Einzugs, yyyy-MM-dd) angegeben werden, sonst wird geschätzt.
|
||||
upkeep:
|
||||
anchor-date: ""
|
||||
|
||||
messages:
|
||||
prefix: "&8[&aChunks&8] &7"
|
||||
no-permission: "&cDazu hast du keine Berechtigung."
|
||||
players-only: "&cNur Spieler können diesen Befehl nutzen."
|
||||
no-land: "&cDu bist in keinem Land Mitglied."
|
||||
land-not-found: "&cLand nicht gefunden oder du bist dort kein Mitglied."
|
||||
reloaded: "&aKonfiguration neu geladen."
|
||||
claim-blocked: "&cAlle freien Chunk-Slots sind nach einem Unclaim noch gesperrt. Nächster Slot frei in &e%time%&c."
|
||||
|
||||
# Gesperrten Slot sofort freischalten (/chunkinfo skip, /chunkinfo gift <Land>).
|
||||
skip:
|
||||
enabled: true
|
||||
# PLAYER = Geld des Spielers (Vault), LAND = Landkasse (nur Besitzer). gift nutzt immer PLAYER.
|
||||
payment: PLAYER
|
||||
# Preis = Restzeit in Stunden * price-per-hour, mindestens min-price.
|
||||
price-per-hour: 25.0
|
||||
min-price: 50.0
|
||||
|
||||
# Auto-Claim-Warteschlange (/chunkinfo queue): claimt vorgemerkte freie Chunks, sobald ein Slot frei ist.
|
||||
# Der Spieler muss dafuer online sein.
|
||||
queue:
|
||||
max: 3
|
||||
|
||||
# Watchlist (/chunkinfo watch): Nachricht, sobald ein fremder Chunk frei wird.
|
||||
watch:
|
||||
max: 5
|
||||
|
||||
# Zeitlich begrenzte Claims (/chunkinfo temp <Tage>): Chunk wird nach Ablauf automatisch freigegeben,
|
||||
# ohne dass dadurch ein Slot gesperrt wird.
|
||||
temp-claims:
|
||||
enabled: true
|
||||
max-days: 30
|
||||
max-per-land: 3
|
||||
|
||||
# Hinweis beim Login: freie Slots, naechster Slot, Upkeep-Warnung.
|
||||
join-reminder:
|
||||
enabled: true
|
||||
upkeep-warning-hours: 48
|
||||
|
||||
top:
|
||||
size: 10
|
||||
@@ -0,0 +1,38 @@
|
||||
name: LandsChunkInfo
|
||||
main: de.landschunkinfo.LandsChunkInfo
|
||||
version: ${project.version}
|
||||
api-version: '1.21'
|
||||
depend: [Lands]
|
||||
softdepend: [Vault, PlaceholderAPI]
|
||||
folia-supported: false
|
||||
description: Zeigt Spielern ihre Chunks und wann wieder ein Chunk frei wird (Unclaim-Cooldown und Upkeep), mit Skip, Queue, Watchlist, Temp-Claims und Platzhaltern.
|
||||
commands:
|
||||
chunkinfo:
|
||||
description: Chunk-Belegung, freie Slots, Upkeep, Queue, Watchlist und mehr
|
||||
usage: /<command> [land|top|skip|gift|queue|watch|temp]
|
||||
aliases: [chunks, chunkslots]
|
||||
permissions:
|
||||
landschunkinfo.use:
|
||||
description: /chunkinfo benutzen
|
||||
default: true
|
||||
landschunkinfo.skip:
|
||||
description: Gesperrten Slot freikaufen
|
||||
default: true
|
||||
landschunkinfo.gift:
|
||||
description: Slot in fremdem Land freikaufen
|
||||
default: true
|
||||
landschunkinfo.queue:
|
||||
description: Auto-Claim-Warteschlange
|
||||
default: true
|
||||
landschunkinfo.watch:
|
||||
description: Chunk-Watchlist
|
||||
default: true
|
||||
landschunkinfo.temp:
|
||||
description: Zeitlich begrenzte Claims
|
||||
default: true
|
||||
landschunkinfo.bypass:
|
||||
description: Unclaim-Cooldown ignorieren
|
||||
default: op
|
||||
landschunkinfo.admin:
|
||||
description: /chunkinfo reload
|
||||
default: op
|
||||
Reference in New Issue
Block a user