Configurable essentials: home tiers by permission, cooldowns, warp cap, RTP radii; rewards config
This commit is contained in:
11 files changed
+276
-16
No files matched your search
@@ -12,6 +12,7 @@ public final class CommandSet {
|
||||
private final GuildCommands guildCommands;
|
||||
private final EconomyCommands economyCommands;
|
||||
private final List<CoreCommand> commands = new ArrayList<>();
|
||||
private final Limits.Cooldowns cooldowns = new Limits.Cooldowns();
|
||||
|
||||
public CommandSet(Env env, EssentialsConfig essentialsConfig) {
|
||||
Path dir = env.dataDir;
|
||||
@@ -22,7 +23,8 @@ public final class CommandSet {
|
||||
GuildCommands guilds = new GuildCommands(env, jobs, economy, essentials, dir.resolve("guild-homes.json"));
|
||||
economyCommands = economy;
|
||||
guildCommands = guilds;
|
||||
add(env, EssentialsCommands.build(env, essentials), "Essentials", () -> env.features.essentials());
|
||||
add(env, EssentialsCommands.build(env, essentials).stream().<CoreCommand>map(c -> new Cooled(c, essentials, cooldowns, env.clock)).toList(),
|
||||
"Essentials", () -> env.features.essentials());
|
||||
add(env, economy.build(), "Economy", () -> env.features.economy());
|
||||
add(env, guilds.build(), "Guilds", () -> env.features.guilds());
|
||||
}
|
||||
@@ -55,6 +57,29 @@ public final class CommandSet {
|
||||
/** Call about once a second from the server thread. */
|
||||
public void tick() { jobs.tick(); }
|
||||
|
||||
/**
|
||||
* Teleport commands can have a cooldown ({@code essentials.cooldowns}). It starts when the command actually teleports (or sends a
|
||||
* request), so a typo doesn't cost the player anything. Players with {@code scopenet.cooldown.bypass} skip it.
|
||||
*/
|
||||
private record Cooled(CoreCommand inner, EssentialsService essentials, Limits.Cooldowns cooldowns, java.util.function.LongSupplier clock) implements CoreCommand {
|
||||
@Override public String name() { return inner.name(); }
|
||||
@Override public List<String> aliases() { return inner.aliases(); }
|
||||
@Override public String permission() { return inner.permission(); }
|
||||
@Override public void run(CorePlayer p, String[] args) {
|
||||
int seconds = essentials.cooldownSeconds(inner.name());
|
||||
boolean cooled = seconds > 0 && !p.hasPermission("scopenet.cooldown.bypass");
|
||||
long now = clock.getAsLong();
|
||||
if (cooled) {
|
||||
long wait = cooldowns.remainingMs(p.uuid(), inner.name(), now);
|
||||
if (wait > 0) { p.send(Format.RED + "Please wait " + Limits.Cooldowns.waitText(wait) + " before using /" + inner.name() + " again."); return; }
|
||||
}
|
||||
long before = essentials.lastAction(p.uuid());
|
||||
inner.run(p, args);
|
||||
if (cooled && essentials.lastAction(p.uuid()) != before) cooldowns.start(p.uuid(), inner.name(), now, seconds);
|
||||
}
|
||||
@Override public List<String> complete(CorePlayer p, String[] args) { return inner.complete(p, args); }
|
||||
}
|
||||
|
||||
/** Checks the feature switch first, then the permission node, so every platform behaves identically. */
|
||||
private record Gated(CoreCommand inner, String system, java.util.function.BooleanSupplier enabled) implements CoreCommand {
|
||||
@Override public String name() { return inner.name(); }
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
package net.scopenet.core;
|
||||
|
||||
public record EssentialsConfig(int maxHomes, int rtpRadius, long tpaTimeoutMs, int rtpAttempts) {
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Every tunable of homes, warps, TPA and RTP. Players can exceed {@code maxHomes} with a permission node
|
||||
* ({@code scopenet.homes.<number>}, or {@code scopenet.homes.unlimited}); {@code cooldowns} are seconds per command
|
||||
* (0 = none) and are skipped for anyone with {@code scopenet.cooldown.bypass}.
|
||||
*/
|
||||
public record EssentialsConfig(int maxHomes, int rtpRadius, long tpaTimeoutMs, int rtpAttempts, int rtpMinRadius, int maxWarps,
|
||||
int homeNameMax, Map<String, Integer> cooldowns) {
|
||||
public static final int TIER_CAP = 500;
|
||||
|
||||
public EssentialsConfig(int maxHomes, int rtpRadius, long tpaTimeoutMs, int rtpAttempts) {
|
||||
this(maxHomes, rtpRadius, tpaTimeoutMs, rtpAttempts, 0, 0, 32, Map.of());
|
||||
}
|
||||
|
||||
public static EssentialsConfig defaults() { return new EssentialsConfig(5, 2500, 60_000, 15); }
|
||||
|
||||
public int cooldown(String command) { return cooldowns == null ? 0 : cooldowns.getOrDefault(command, 0); }
|
||||
}
|
||||
@@ -47,6 +47,8 @@ public final class EssentialsService {
|
||||
|
||||
public void configure(EssentialsConfig next) { this.config = next; }
|
||||
|
||||
public int cooldownSeconds(String command) { return config.cooldown(command); }
|
||||
|
||||
// ---- persistence -------------------------------------------------------
|
||||
|
||||
private void load() {
|
||||
@@ -113,8 +115,10 @@ public final class EssentialsService {
|
||||
public void setHome(CorePlayer p, String[] args) {
|
||||
String name = args.length > 0 ? args[0].toLowerCase(Locale.ROOT) : "home";
|
||||
Map<String, Pos> mine = homes.computeIfAbsent(p.uuid(), k -> new HashMap<>());
|
||||
if (!mine.containsKey(name) && mine.size() >= config.maxHomes()) {
|
||||
p.send(RED + "You have reached your limit of " + config.maxHomes() + " homes.");
|
||||
if (name.length() > config.homeNameMax()) { p.send(RED + "Home names can be at most " + config.homeNameMax() + " characters."); return; }
|
||||
int limit = Limits.tier(p::hasPermission, "scopenet.homes.", config.maxHomes(), EssentialsConfig.TIER_CAP);
|
||||
if (!mine.containsKey(name) && mine.size() >= limit) {
|
||||
p.send(RED + "You have reached your limit of " + Limits.show(limit) + " homes.");
|
||||
return;
|
||||
}
|
||||
mine.put(name, p.pos());
|
||||
@@ -161,6 +165,10 @@ public final class EssentialsService {
|
||||
}
|
||||
|
||||
public void setWarp(CorePlayer p, String name) {
|
||||
if (config.maxWarps() > 0 && !warps.containsKey(name.toLowerCase(Locale.ROOT)) && warps.size() >= config.maxWarps()) {
|
||||
p.send(RED + "The server already has " + config.maxWarps() + " warps. Delete one first.");
|
||||
return;
|
||||
}
|
||||
warps.put(name.toLowerCase(Locale.ROOT), p.pos());
|
||||
save();
|
||||
p.send(GREEN + "Warp " + YELLOW + name.toLowerCase(Locale.ROOT) + GREEN + " set.");
|
||||
@@ -190,6 +198,7 @@ public final class EssentialsService {
|
||||
CorePlayer target = found.get();
|
||||
if (target.uuid().equals(p.uuid())) { p.send(RED + "You cannot teleport to yourself."); return; }
|
||||
pendingTpa.put(target.uuid(), Map.entry(p.uuid(), clock.getAsLong()));
|
||||
lastAction.put(p.uuid(), clock.getAsLong());
|
||||
p.send(GREEN + "Teleport request sent to " + YELLOW + target.name() + GREEN + ".");
|
||||
target.send(YELLOW + p.name() + GREEN + " has sent you a teleport request.");
|
||||
target.send(GRAY + "Type " + YELLOW + "/tpaccept" + GRAY + " to accept or " + RED + "/tpdeny" + GRAY
|
||||
@@ -223,11 +232,13 @@ public final class EssentialsService {
|
||||
/** Must run on the server thread, because it reads chunks. */
|
||||
public void rtp(CorePlayer p) {
|
||||
int radius = config.rtpRadius();
|
||||
int min = Math.min(config.rtpMinRadius(), radius - 1);
|
||||
String world = p.pos().world();
|
||||
p.send(GRAY + "Searching for a safe wilderness destination...");
|
||||
for (int i = 0; i < config.rtpAttempts(); i++) {
|
||||
int x = rng.nextInt(radius * 2) - radius;
|
||||
int z = rng.nextInt(radius * 2) - radius;
|
||||
if (Math.max(Math.abs(x), Math.abs(z)) < min) continue; // too close to the centre
|
||||
Optional<Pos> spot = platform.safeSurface(world, x, z);
|
||||
if (spot.isPresent()) {
|
||||
Pos at = spot.get();
|
||||
@@ -242,7 +253,13 @@ public final class EssentialsService {
|
||||
/** Teleport and remember where they were for /back. */
|
||||
public void teleportTo(CorePlayer p, Pos destination) { go(p, destination); }
|
||||
|
||||
/** When a player last teleported or sent a teleport request (for cooldowns). */
|
||||
public long lastAction(UUID id) { return lastAction.getOrDefault(id, -1L); }
|
||||
|
||||
private final Map<UUID, Long> lastAction = new ConcurrentHashMap<>();
|
||||
|
||||
private void go(CorePlayer p, Pos destination) {
|
||||
lastAction.put(p.uuid(), clock.getAsLong());
|
||||
remember(p.uuid(), p.pos());
|
||||
p.teleport(destination);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package net.scopenet.core;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/** Per-player limits that permission nodes can raise, and command cooldowns. */
|
||||
public final class Limits {
|
||||
private Limits() {}
|
||||
|
||||
/**
|
||||
* The highest tier a player has, e.g. {@code scopenet.homes.10}, otherwise {@code base}. {@code <prefix>unlimited}
|
||||
* means no limit. Only values above {@code base} are looked for, up to {@code cap}.
|
||||
*/
|
||||
public static int tier(Predicate<String> has, String prefix, int base, int cap) {
|
||||
if (has.test(prefix + "unlimited")) return Integer.MAX_VALUE;
|
||||
for (int n = cap; n > base; n--) if (has.test(prefix + n)) return n;
|
||||
return base;
|
||||
}
|
||||
|
||||
public static String show(int limit) { return limit == Integer.MAX_VALUE ? "unlimited" : Integer.toString(limit); }
|
||||
|
||||
/** When each player may use each command again. Not thread-safe: use from the server thread. */
|
||||
public static final class Cooldowns {
|
||||
private final Map<String, Long> until = new HashMap<>();
|
||||
|
||||
public long remainingMs(UUID player, String command, long now) {
|
||||
Long end = until.get(player + ":" + command);
|
||||
return end == null ? 0 : Math.max(0, end - now);
|
||||
}
|
||||
|
||||
public void start(UUID player, String command, long now, int seconds) {
|
||||
if (seconds > 0) until.put(player + ":" + command, now + seconds * 1000L);
|
||||
if (until.size() > 4000) until.values().removeIf(end -> end < now);
|
||||
}
|
||||
|
||||
public static String waitText(long ms) { return Math.max(1, (ms + 999) / 1000) + "s"; }
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ class EssentialsServiceTest {
|
||||
public Pos pos() { return pos; }
|
||||
public void send(String m) { inbox.add(m.replaceAll("§.", "")); }
|
||||
public void teleport(Pos p) { pos = p; }
|
||||
public boolean hasPermission(String n) { return true; }
|
||||
public boolean hasPermission(String n) { return !n.startsWith("scopenet.homes.") && !n.equals("scopenet.cooldown.bypass"); }
|
||||
public long playtimeSeconds() { return 0; }
|
||||
public java.util.Optional<Item> heldItem() { return java.util.Optional.empty(); }
|
||||
public void clearHeld() { }
|
||||
|
||||
@@ -17,6 +17,8 @@ final class Kit {
|
||||
Pos pos = new Pos("minecraft:overworld", 0, 64, 0, 0, 0);
|
||||
final List<String> inbox = new ArrayList<>();
|
||||
final Set<String> denied = new HashSet<>();
|
||||
/** Nodes that grant extras (limit tiers, cooldown bypass) are off unless given here. */
|
||||
final Set<String> granted = new HashSet<>();
|
||||
Item held;
|
||||
final List<Item> inventory = new ArrayList<>();
|
||||
long playtime = 3700;
|
||||
@@ -26,7 +28,7 @@ final class Kit {
|
||||
public Pos pos() { return pos; }
|
||||
public void send(String m) { inbox.add(Format.plain(m)); }
|
||||
public void teleport(Pos p) { pos = p; }
|
||||
public boolean hasPermission(String n) { return !denied.contains(n); }
|
||||
public boolean hasPermission(String n) { return granted.contains(n) || (!n.startsWith("scopenet.homes.") && !n.equals("scopenet.cooldown.bypass") && !denied.contains(n)); }
|
||||
public long playtimeSeconds() { return playtime; }
|
||||
public Optional<Item> heldItem() { return Optional.ofNullable(held); }
|
||||
public void clearHeld() { held = null; }
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package net.scopenet.core;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class LimitsTest {
|
||||
@Test void permissionTiersRaiseALimit() {
|
||||
assertEquals(5, Limits.tier(n -> false, "scopenet.homes.", 5, 500));
|
||||
assertEquals(10, Limits.tier(Set.of("scopenet.homes.10")::contains, "scopenet.homes.", 5, 500));
|
||||
assertEquals(25, Limits.tier(Set.of("scopenet.homes.10", "scopenet.homes.25", "scopenet.homes.3")::contains, "scopenet.homes.", 5, 500), "highest wins");
|
||||
assertEquals(5, Limits.tier(Set.of("scopenet.homes.3")::contains, "scopenet.homes.", 5, 500), "a lower tier never reduces the base");
|
||||
assertEquals(Integer.MAX_VALUE, Limits.tier(Set.of("scopenet.homes.unlimited")::contains, "scopenet.homes.", 5, 500));
|
||||
assertEquals("unlimited", Limits.show(Integer.MAX_VALUE));
|
||||
}
|
||||
|
||||
@Test void homesFollowConfigAndPermissions(@TempDir Path dir) {
|
||||
Kit kit = new Kit(dir);
|
||||
Kit.Player steve = kit.platform.add("Steve");
|
||||
EssentialsConfig cfg = new EssentialsConfig(2, 2500, 60_000, 15, 0, 1, 8, Map.of());
|
||||
EssentialsService svc = new EssentialsService(kit.platform, dir.resolve("e.json"), cfg, kit.now::get, kit.env.rng);
|
||||
svc.setHome(steve, new String[]{"a"});
|
||||
svc.setHome(steve, new String[]{"b"});
|
||||
svc.setHome(steve, new String[]{"c"});
|
||||
assertTrue(steve.last().contains("limit of 2"));
|
||||
steve.granted.add("scopenet.homes.4");
|
||||
svc.setHome(steve, new String[]{"c"});
|
||||
svc.setHome(steve, new String[]{"d"});
|
||||
svc.setHome(steve, new String[]{"e"});
|
||||
assertTrue(steve.last().contains("limit of 4"), steve.last());
|
||||
svc.setHome(steve, new String[]{"waytoolongname"});
|
||||
assertTrue(steve.last().contains("at most 8"), steve.last());
|
||||
svc.setWarp(steve, "spawn");
|
||||
svc.setWarp(steve, "pvp");
|
||||
assertTrue(steve.last().contains("already has 1 warps"), steve.last());
|
||||
}
|
||||
|
||||
@Test void teleportCommandsCoolDownOnlyAfterTeleporting(@TempDir Path dir) {
|
||||
Kit kit = new Kit(dir);
|
||||
Kit.Player steve = kit.platform.add("Steve");
|
||||
EssentialsConfig cfg = new EssentialsConfig(5, 2500, 60_000, 15, 0, 0, 32, Map.of("home", 30, "warp", 10));
|
||||
CommandSet set = new CommandSet(kit.env, cfg);
|
||||
set.essentials.setHome(steve, new String[]{"base"});
|
||||
set.run("home", steve, "nope"); // unknown home: nothing happened, no cooldown
|
||||
set.run("home", steve, "base");
|
||||
assertTrue(steve.last().contains("Teleported to home"), steve.last());
|
||||
set.run("home", steve, "base");
|
||||
assertTrue(steve.last().contains("Please wait 30s"), steve.last());
|
||||
kit.now.addAndGet(31_000);
|
||||
set.run("home", steve, "base");
|
||||
assertTrue(steve.last().contains("Teleported to home"), steve.last());
|
||||
set.run("warp", steve, "missing");
|
||||
set.run("warp", steve, "missing");
|
||||
assertFalse(steve.last().contains("Please wait"), "a failed warp costs nothing");
|
||||
steve.granted.add("scopenet.cooldown.bypass");
|
||||
set.run("home", steve, "base");
|
||||
assertTrue(steve.last().contains("Teleported to home"), "bypass skips cooldowns");
|
||||
}
|
||||
|
||||
@Test void rtpKeepsAwayFromTheCentre(@TempDir Path dir) {
|
||||
Kit kit = new Kit(dir);
|
||||
Kit.Player steve = kit.platform.add("Steve");
|
||||
EssentialsConfig cfg = new EssentialsConfig(5, 400, 60_000, 50, 300, 0, 32, Map.of());
|
||||
EssentialsService svc = new EssentialsService(kit.platform, dir.resolve("e.json"), cfg, kit.now::get, new java.util.Random(7));
|
||||
for (int i = 0; i < 20; i++) {
|
||||
svc.rtp(steve);
|
||||
int x = (int) steve.pos.x(), z = (int) steve.pos.z();
|
||||
assertTrue(Math.max(Math.abs(x), Math.abs(z)) >= 300, "landed at " + x + "," + z);
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -33,8 +33,12 @@ final class FabricConfig {
|
||||
String text(String key, String fallback) { return p.getProperty(key, fallback).trim(); }
|
||||
|
||||
EssentialsConfig essentials() {
|
||||
java.util.Map<String, Integer> cooldowns = new java.util.HashMap<>();
|
||||
for (String cmd : new String[]{"spawn", "home", "back", "tpa", "rtp", "warp"}) cooldowns.put(cmd, Math.max(0, integer("essentials.cooldown." + cmd, 0)));
|
||||
return new EssentialsConfig(Math.max(0, integer("essentials.max_homes", 5)), Math.max(10, integer("essentials.rtp_radius", 2500)),
|
||||
Math.max(5, integer("essentials.tpa_timeout_seconds", 60)) * 1000L, 15);
|
||||
Math.max(5, integer("essentials.tpa_timeout_seconds", 60)) * 1000L, Math.max(1, integer("essentials.rtp_attempts", 15)),
|
||||
Math.max(0, integer("essentials.rtp_min_radius", 0)), Math.max(0, integer("essentials.max_warps", 0)),
|
||||
Math.max(1, integer("essentials.home_name_max_length", 32)), cooldowns);
|
||||
}
|
||||
|
||||
Features features(Settings s) {
|
||||
|
||||
@@ -16,8 +16,11 @@ final class Perms {
|
||||
|
||||
static boolean luckPermsPresent() { return LUCKPERMS; }
|
||||
|
||||
/** Nodes that grant extras (limit tiers like scopenet.homes.10): nobody has them unless a permissions system says so. */
|
||||
static boolean isGrantNode(String node) { return node.startsWith("scopenet.homes.") || node.startsWith("scopenet.limit."); }
|
||||
|
||||
static boolean isAdminNode(String node) {
|
||||
return node.equals("scopenet.claims.bypass") || node.startsWith("scopenet.admin")
|
||||
return node.equals("scopenet.claims.bypass") || node.equals("scopenet.cooldown.bypass") || node.startsWith("scopenet.admin")
|
||||
|| node.equals("scopenet.command.scopenet.status") || node.equals("scopenet.command.scopenet.reload");
|
||||
}
|
||||
|
||||
@@ -30,6 +33,7 @@ final class Perms {
|
||||
// LuckPerms not ready yet or an API change: fall back to the defaults below.
|
||||
}
|
||||
}
|
||||
if (isGrantNode(node)) return false;
|
||||
if (isAdminNode(node)) return player.hasPermissions(2);
|
||||
return everyoneByDefault || player.hasPermissions(2);
|
||||
}
|
||||
|
||||
+45
-6
@@ -133,7 +133,9 @@ public final class EssentialsHandler implements CommandExecutor, Listener {
|
||||
Map<String, Location> playerHomes = homes.get(player.getUniqueId());
|
||||
if (playerHomes != null && playerHomes.containsKey(homeName)) {
|
||||
player.closeInventory();
|
||||
if (coolingDown(player, "home")) return;
|
||||
player.teleport(playerHomes.get(homeName));
|
||||
startCooldown(player, "home");
|
||||
player.sendMessage(ChatColor.GREEN + "Teleported to home: " + ChatColor.YELLOW + homeName);
|
||||
}
|
||||
} else if (title.equals(GUI_WARPS_TITLE)) {
|
||||
@@ -143,7 +145,9 @@ public final class EssentialsHandler implements CommandExecutor, Listener {
|
||||
String warpName = ChatColor.stripColor(item.getItemMeta().getDisplayName()).replace("Warp: ", "").trim().toLowerCase();
|
||||
if (warps.containsKey(warpName)) {
|
||||
player.closeInventory();
|
||||
if (coolingDown(player, "warp")) return;
|
||||
player.teleport(warps.get(warpName));
|
||||
startCooldown(player, "warp");
|
||||
player.sendMessage(ChatColor.GREEN + "Teleported to warp: " + ChatColor.YELLOW + warpName);
|
||||
}
|
||||
}
|
||||
@@ -158,6 +162,9 @@ public final class EssentialsHandler implements CommandExecutor, Listener {
|
||||
|
||||
if (!plugin.getConfig().getBoolean("essentials.enabled", true)) { player.sendMessage(ChatColor.RED + "Essentials are disabled."); return true; }
|
||||
String cmd = command.getName().toLowerCase();
|
||||
boolean cooled = cooldownSeconds(cmd) > 0 && !player.hasPermission("scopenet.cooldown.bypass");
|
||||
if (cooled && coolingDown(player, cmd)) return true;
|
||||
Location before = player.getLocation();
|
||||
switch (cmd) {
|
||||
case "spawn" -> handleSpawn(player);
|
||||
case "home" -> handleHome(player, args);
|
||||
@@ -178,6 +185,7 @@ public final class EssentialsHandler implements CommandExecutor, Listener {
|
||||
case "playtime" -> handlePlaytime(player, args);
|
||||
default -> { return false; }
|
||||
}
|
||||
if (cooled && ((cmd.equals("tpa") && args.length > 0) || cmd.equals("rtp") || !before.equals(player.getLocation()))) startCooldown(player, cmd);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -263,13 +271,36 @@ public final class EssentialsHandler implements CommandExecutor, Listener {
|
||||
player.openInventory(inv);
|
||||
}
|
||||
|
||||
// ---- limits and cooldowns (the same rules as the Fabric mod; see config.yml) ----
|
||||
|
||||
private final net.scopenet.core.Limits.Cooldowns cooldowns = new net.scopenet.core.Limits.Cooldowns();
|
||||
|
||||
private int tpaTimeoutSeconds() { return Math.max(5, plugin.getConfig().getInt("essentials.tpa-timeout-seconds", 60)); }
|
||||
|
||||
private int cooldownSeconds(String cmd) { return Math.max(0, plugin.getConfig().getInt("essentials.cooldowns." + cmd, 0)); }
|
||||
|
||||
/** True (and tells the player) if they must still wait before using this command. */
|
||||
private boolean coolingDown(Player player, String cmd) {
|
||||
if (cooldownSeconds(cmd) <= 0 || player.hasPermission("scopenet.cooldown.bypass")) return false;
|
||||
long wait = cooldowns.remainingMs(player.getUniqueId(), cmd, System.currentTimeMillis());
|
||||
if (wait <= 0) return false;
|
||||
player.sendMessage(ChatColor.RED + "Please wait " + net.scopenet.core.Limits.Cooldowns.waitText(wait) + " before using /" + cmd + " again.");
|
||||
return true;
|
||||
}
|
||||
|
||||
private void startCooldown(Player player, String cmd) {
|
||||
if (!player.hasPermission("scopenet.cooldown.bypass")) cooldowns.start(player.getUniqueId(), cmd, System.currentTimeMillis(), cooldownSeconds(cmd));
|
||||
}
|
||||
|
||||
private void handleSetHome(Player player, String[] args) {
|
||||
String homeName = args.length > 0 ? args[0].toLowerCase() : "home";
|
||||
int maxHomes = plugin.getConfig().getInt("essentials.max-homes", 5);
|
||||
int nameMax = Math.max(1, plugin.getConfig().getInt("essentials.home-name-max-length", 32));
|
||||
if (homeName.length() > nameMax) { player.sendMessage(ChatColor.RED + "Home names can be at most " + nameMax + " characters."); return; }
|
||||
int maxHomes = net.scopenet.core.Limits.tier(player::hasPermission, "scopenet.homes.", plugin.getConfig().getInt("essentials.max-homes", 5), 500);
|
||||
Map<String, Location> playerHomes = homes.computeIfAbsent(player.getUniqueId(), k -> new HashMap<>());
|
||||
|
||||
if (!playerHomes.containsKey(homeName) && playerHomes.size() >= maxHomes) {
|
||||
player.sendMessage(ChatColor.RED + "You have reached your limit of " + maxHomes + " homes.");
|
||||
player.sendMessage(ChatColor.RED + "You have reached your limit of " + net.scopenet.core.Limits.show(maxHomes) + " homes.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -324,13 +355,13 @@ public final class EssentialsHandler implements CommandExecutor, Listener {
|
||||
|
||||
player.sendMessage(ChatColor.GREEN + "Teleport request sent to " + ChatColor.YELLOW + target.getName() + ChatColor.GREEN + ".");
|
||||
target.sendMessage(ChatColor.YELLOW + player.getName() + ChatColor.GREEN + " has sent you a teleport request.");
|
||||
target.sendMessage(ChatColor.GRAY + "Type " + ChatColor.YELLOW + "/tpaccept" + ChatColor.GRAY + " to accept or " + ChatColor.RED + "/tpdeny" + ChatColor.GRAY + " to deny (expires in 60s).");
|
||||
target.sendMessage(ChatColor.GRAY + "Type " + ChatColor.YELLOW + "/tpaccept" + ChatColor.GRAY + " to accept or " + ChatColor.RED + "/tpdeny" + ChatColor.GRAY + " to deny (expires in " + tpaTimeoutSeconds() + "s).");
|
||||
}
|
||||
|
||||
private void handleTpAccept(Player player) {
|
||||
UUID requesterId = pendingTpa.get(player.getUniqueId());
|
||||
Long time = tpaTimestamp.get(player.getUniqueId());
|
||||
if (requesterId == null || time == null || System.currentTimeMillis() - time > 60_000) {
|
||||
if (requesterId == null || time == null || System.currentTimeMillis() - time > tpaTimeoutSeconds() * 1000L) {
|
||||
pendingTpa.remove(player.getUniqueId());
|
||||
tpaTimestamp.remove(player.getUniqueId());
|
||||
player.sendMessage(ChatColor.RED + "You have no active teleport requests.");
|
||||
@@ -366,15 +397,18 @@ public final class EssentialsHandler implements CommandExecutor, Listener {
|
||||
}
|
||||
|
||||
private void handleRtp(Player player) {
|
||||
int radius = plugin.getConfig().getInt("essentials.rtp-radius", 2500);
|
||||
int radius = Math.max(10, plugin.getConfig().getInt("essentials.rtp-radius", 2500));
|
||||
int minRadius = Math.min(plugin.getConfig().getInt("essentials.rtp-min-radius", 0), radius - 1);
|
||||
int attempts = Math.max(1, plugin.getConfig().getInt("essentials.rtp-attempts", 15));
|
||||
World world = player.getWorld();
|
||||
Random rng = new Random();
|
||||
|
||||
player.sendMessage(ChatColor.GRAY + "Searching for a safe wilderness destination...");
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
for (int i = 0; i < 15; i++) {
|
||||
for (int i = 0; i < attempts; i++) {
|
||||
int x = rng.nextInt(radius * 2) - radius;
|
||||
int z = rng.nextInt(radius * 2) - radius;
|
||||
if (Math.max(Math.abs(x), Math.abs(z)) < minRadius) continue;
|
||||
int y = world.getHighestBlockYAt(x, z);
|
||||
Block block = world.getBlockAt(x, y - 1, z);
|
||||
if (block.getType().isSolid() && block.getType() != Material.LAVA && block.getType() != Material.WATER) {
|
||||
@@ -471,6 +505,11 @@ public final class EssentialsHandler implements CommandExecutor, Listener {
|
||||
if (!warpAdmin(player)) return;
|
||||
String name = warpName(player, args, "/setwarp <name>");
|
||||
if (name == null) return;
|
||||
int maxWarps = plugin.getConfig().getInt("essentials.max-warps", 0);
|
||||
if (maxWarps > 0 && !warps.containsKey(name) && warps.size() >= maxWarps) {
|
||||
player.sendMessage(ChatColor.RED + "The server already has " + maxWarps + " warps. Delete one first.");
|
||||
return;
|
||||
}
|
||||
warps.put(name, player.getLocation());
|
||||
saveWarps();
|
||||
player.sendMessage(ChatColor.GREEN + "Warp set: " + name);
|
||||
|
||||
@@ -46,10 +46,30 @@ social:
|
||||
essentials:
|
||||
# Essentials commands (/spawn, /home, /sethome, /delhome, /back, /tpa, /tpaccept, /tpdeny, /rtp, /warp, /playtime)
|
||||
enabled: true
|
||||
# Maximum homes per player
|
||||
# Homes every player can set. Raise it for a rank with a permission node: scopenet.homes.10 gives 10 homes,
|
||||
# scopenet.homes.25 gives 25 (the highest one a player has wins), scopenet.homes.unlimited removes the limit.
|
||||
# Operators and anyone with "*" get the top tier.
|
||||
max-homes: 5
|
||||
# RTP search radius
|
||||
# Longest allowed home name.
|
||||
home-name-max-length: 32
|
||||
# Most warps the server may have (0 = no limit).
|
||||
max-warps: 0
|
||||
# How long a /tpa request stays open.
|
||||
tpa-timeout-seconds: 60
|
||||
# /rtp picks a random spot between rtp-min-radius and rtp-radius blocks from the centre (0,0).
|
||||
rtp-radius: 2500
|
||||
rtp-min-radius: 0
|
||||
# How many random spots /rtp tries before giving up.
|
||||
rtp-attempts: 15
|
||||
# Seconds a player must wait between uses (0 = no cooldown). The wait starts when the command actually
|
||||
# teleports, so a typo costs nothing. Players with scopenet.cooldown.bypass skip all of these.
|
||||
cooldowns:
|
||||
spawn: 0
|
||||
home: 0
|
||||
back: 0
|
||||
tpa: 0
|
||||
rtp: 0
|
||||
warp: 0
|
||||
|
||||
map:
|
||||
# The SCOPENET Map: this server renders its world to map tiles in the background and sends only what changed,
|
||||
@@ -96,6 +116,23 @@ integrations:
|
||||
enabled: true
|
||||
report-interval-seconds: 30
|
||||
|
||||
# ==============================================================================
|
||||
# Rewards
|
||||
# Quests, achievements and rank milestones can give more than XP: money, items, permissions, groups, claim
|
||||
# chunks, badges and messages (set them in the admin panel). The panel hands items, permissions and commands
|
||||
# to this server when the player is online. Permissions and groups are applied with console commands, so any
|
||||
# permissions plugin works; the defaults are LuckPerms'. Placeholders: {player} {uuid} {node} {value} {duration} {group}.
|
||||
# ==============================================================================
|
||||
rewards:
|
||||
enabled: true
|
||||
# Let the panel run its own console commands as rewards. Off by default: anyone who can edit rewards in the panel
|
||||
# could otherwise run commands on this server. Only switch it on if you trust everyone with panel admin access.
|
||||
allow-commands: false
|
||||
permission-command: "lp user {player} permission set {node} true"
|
||||
permission-deny-command: "lp user {player} permission set {node} false"
|
||||
permission-temp-command: "lp user {player} permission settemp {node} {value} {duration}"
|
||||
group-command: "lp user {player} parent add {group}"
|
||||
|
||||
# ==============================================================================
|
||||
# Client mod link
|
||||
# Sends balance, level, guild and claim info to players who have the optional SCOPENET
|
||||
|
||||
Reference in new issue
Block a user