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"; }
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user