Fresh upload
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.dirtsmp</groupId>
|
||||
<artifactId>DirtSMP</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>DirtSMP</name>
|
||||
<description>DirtbagMC-themed configurable world-border progression for Paper SMP servers.</description>
|
||||
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
<paper.version>1.21.8-R0.1-SNAPSHOT</paper.version>
|
||||
<placeholderapi.version>2.11.6</placeholderapi.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>papermc</id>
|
||||
<url>https://repo.papermc.io/repository/maven-public/</url>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>placeholderapi</id>
|
||||
<url>https://repo.helpch.at/releases/</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.papermc.paper</groupId>
|
||||
<artifactId>paper-api</artifactId>
|
||||
<version>${paper.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>me.clip</groupId>
|
||||
<artifactId>placeholderapi</artifactId>
|
||||
<version>${placeholderapi.version}</version>
|
||||
<scope>provided</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<finalName>DirtSMP-${project.version}</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.13.0</version>
|
||||
<configuration>
|
||||
<release>${java.version}</release>
|
||||
<showWarnings>true</showWarnings>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>3.4.2</version>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
|
||||
</manifest>
|
||||
</archive>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,180 @@
|
||||
package com.dirtsmp.dirtsmp;
|
||||
|
||||
import com.dirtsmp.dirtsmp.command.DirtSMPCommand;
|
||||
import com.dirtsmp.dirtsmp.config.ConfigManager;
|
||||
import com.dirtsmp.dirtsmp.config.MessageManager;
|
||||
import com.dirtsmp.dirtsmp.gui.AdminGuiManager;
|
||||
import com.dirtsmp.dirtsmp.hook.PlaceholderHook;
|
||||
import com.dirtsmp.dirtsmp.listener.PlayerListener;
|
||||
import com.dirtsmp.dirtsmp.service.BorderManager;
|
||||
import com.dirtsmp.dirtsmp.service.CommandHookManager;
|
||||
import com.dirtsmp.dirtsmp.service.HistoryLogger;
|
||||
import com.dirtsmp.dirtsmp.service.SoftBorderManager;
|
||||
import com.dirtsmp.dirtsmp.service.TriggerEvaluator;
|
||||
import com.dirtsmp.dirtsmp.service.WebhookNotifier;
|
||||
import com.dirtsmp.dirtsmp.state.PlayerStatsManager;
|
||||
import com.dirtsmp.dirtsmp.state.StateManager;
|
||||
import com.dirtsmp.dirtsmp.task.ScheduleManager;
|
||||
import java.io.IOException;
|
||||
import java.util.logging.Level;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.command.PluginCommand;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
public final class DirtSMPPlugin extends JavaPlugin {
|
||||
private ConfigManager configManager;
|
||||
private MessageManager messageManager;
|
||||
private StateManager stateManager;
|
||||
private PlayerStatsManager playerStatsManager;
|
||||
private TriggerEvaluator triggerEvaluator;
|
||||
private BorderManager borderManager;
|
||||
private ScheduleManager scheduleManager;
|
||||
private AdminGuiManager adminGuiManager;
|
||||
private SoftBorderManager softBorderManager;
|
||||
private PlaceholderHook placeholderHook;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
try {
|
||||
bootstrap();
|
||||
getLogger().info("DirtSMP enabled with " + configManager.worlds().size() + " configured world(s).");
|
||||
} catch (RuntimeException ex) {
|
||||
getLogger().log(Level.SEVERE, "DirtSMP could not enable cleanly.", ex);
|
||||
getServer().getPluginManager().disablePlugin(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
if (scheduleManager != null) {
|
||||
scheduleManager.stop();
|
||||
}
|
||||
if (softBorderManager != null) {
|
||||
softBorderManager.stop();
|
||||
}
|
||||
Bukkit.getScheduler().cancelTasks(this);
|
||||
if (adminGuiManager != null) {
|
||||
adminGuiManager.closeOpenInventories();
|
||||
}
|
||||
if (placeholderHook != null) {
|
||||
placeholderHook.unregister();
|
||||
}
|
||||
HandlerList.unregisterAll(this);
|
||||
|
||||
if (stateManager != null) {
|
||||
stateManager.saveQuietly();
|
||||
stateManager.clearRuntimeState();
|
||||
}
|
||||
if (playerStatsManager != null) {
|
||||
playerStatsManager.saveQuietly();
|
||||
playerStatsManager.clearRuntimeState();
|
||||
}
|
||||
getLogger().info("DirtSMP disabled.");
|
||||
}
|
||||
|
||||
private void bootstrap() {
|
||||
configManager = new ConfigManager(this);
|
||||
configManager.load();
|
||||
messageManager = new MessageManager(configManager);
|
||||
stateManager = new StateManager(this, configManager);
|
||||
playerStatsManager = new PlayerStatsManager(this);
|
||||
stateManager.load();
|
||||
playerStatsManager.load();
|
||||
|
||||
CommandHookManager commandHookManager = new CommandHookManager(configManager, messageManager);
|
||||
WebhookNotifier webhookNotifier = new WebhookNotifier(this, configManager, messageManager);
|
||||
HistoryLogger historyLogger = new HistoryLogger(this, configManager);
|
||||
triggerEvaluator = new TriggerEvaluator(playerStatsManager);
|
||||
borderManager = new BorderManager(this, configManager, messageManager, stateManager, playerStatsManager, commandHookManager, webhookNotifier, historyLogger);
|
||||
scheduleManager = new ScheduleManager(this, configManager, stateManager, playerStatsManager, borderManager, triggerEvaluator, messageManager);
|
||||
adminGuiManager = new AdminGuiManager(this, configManager, messageManager, stateManager, borderManager, triggerEvaluator);
|
||||
softBorderManager = new SoftBorderManager(this, configManager, messageManager, stateManager);
|
||||
|
||||
registerCommands();
|
||||
Bukkit.getPluginManager().registerEvents(new PlayerListener(this, playerStatsManager, borderManager, configManager), this);
|
||||
Bukkit.getPluginManager().registerEvents(adminGuiManager, this);
|
||||
Bukkit.getPluginManager().registerEvents(softBorderManager, this);
|
||||
registerPlaceholders();
|
||||
|
||||
borderManager.applyStartupRules();
|
||||
softBorderManager.start();
|
||||
scheduleManager.start();
|
||||
}
|
||||
|
||||
private void registerCommands() {
|
||||
PluginCommand command = getCommand("dirtsmp");
|
||||
if (command == null) {
|
||||
throw new IllegalStateException("plugin.yml is missing the dirtsmp command.");
|
||||
}
|
||||
DirtSMPCommand commandHandler = new DirtSMPCommand(this);
|
||||
command.setExecutor(commandHandler);
|
||||
command.setTabCompleter(commandHandler);
|
||||
}
|
||||
|
||||
private void registerPlaceholders() {
|
||||
if (Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI")) {
|
||||
placeholderHook = new PlaceholderHook(this);
|
||||
placeholderHook.register();
|
||||
}
|
||||
}
|
||||
|
||||
public void reloadDirtSMP() {
|
||||
if (scheduleManager != null) {
|
||||
scheduleManager.stop();
|
||||
}
|
||||
if (softBorderManager != null) {
|
||||
softBorderManager.stop();
|
||||
}
|
||||
if (stateManager != null) {
|
||||
stateManager.saveQuietly();
|
||||
}
|
||||
if (playerStatsManager != null) {
|
||||
playerStatsManager.saveQuietly();
|
||||
}
|
||||
|
||||
configManager.load();
|
||||
stateManager.load();
|
||||
playerStatsManager.load();
|
||||
borderManager.applyStartupRules();
|
||||
softBorderManager.start();
|
||||
scheduleManager.start();
|
||||
}
|
||||
|
||||
public void saveAll() {
|
||||
try {
|
||||
stateManager.save();
|
||||
playerStatsManager.save();
|
||||
} catch (IOException ex) {
|
||||
getLogger().log(Level.WARNING, "Could not save all DirtSMP data.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public ConfigManager configManager() {
|
||||
return configManager;
|
||||
}
|
||||
|
||||
public MessageManager messageManager() {
|
||||
return messageManager;
|
||||
}
|
||||
|
||||
public StateManager stateManager() {
|
||||
return stateManager;
|
||||
}
|
||||
|
||||
public PlayerStatsManager playerStatsManager() {
|
||||
return playerStatsManager;
|
||||
}
|
||||
|
||||
public TriggerEvaluator triggerEvaluator() {
|
||||
return triggerEvaluator;
|
||||
}
|
||||
|
||||
public BorderManager borderManager() {
|
||||
return borderManager;
|
||||
}
|
||||
|
||||
public AdminGuiManager adminGuiManager() {
|
||||
return adminGuiManager;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,592 @@
|
||||
package com.dirtsmp.dirtsmp.command;
|
||||
|
||||
import com.dirtsmp.dirtsmp.DirtSMPPlugin;
|
||||
import com.dirtsmp.dirtsmp.config.MessageManager;
|
||||
import com.dirtsmp.dirtsmp.model.ExpansionReason;
|
||||
import com.dirtsmp.dirtsmp.model.ExpansionResult;
|
||||
import com.dirtsmp.dirtsmp.model.LegacyAccessReport;
|
||||
import com.dirtsmp.dirtsmp.model.PhaseDefinition;
|
||||
import com.dirtsmp.dirtsmp.model.WorldRule;
|
||||
import com.dirtsmp.dirtsmp.service.TimeUtil;
|
||||
import com.dirtsmp.dirtsmp.state.WorldProgress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.OptionalDouble;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.Statistic;
|
||||
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.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public final class DirtSMPCommand implements CommandExecutor, TabCompleter {
|
||||
private static final List<String> SUBCOMMANDS = List.of("help", "reload", "status", "next", "legacy", "legacycheck", "tpborder", "tpborderside", "add", "expand", "setsize", "pause", "resume", "reset", "gui", "save");
|
||||
private static final List<String> BORDER_SIDES = List.of("north", "south", "east", "west");
|
||||
|
||||
private final DirtSMPPlugin plugin;
|
||||
|
||||
public DirtSMPCommand(DirtSMPPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
|
||||
if (args.length == 0 || args[0].equalsIgnoreCase("help")) {
|
||||
plugin.messageManager().sendList(sender, "commands.help", Map.of());
|
||||
return true;
|
||||
}
|
||||
|
||||
String sub = args[0].toLowerCase(Locale.ROOT);
|
||||
switch (sub) {
|
||||
case "reload" -> reload(sender);
|
||||
case "status" -> status(sender, args);
|
||||
case "next" -> next(sender, args);
|
||||
case "legacy" -> legacy(sender, args);
|
||||
case "legacycheck" -> legacyCheck(sender, args);
|
||||
case "tpborder", "tpborderside" -> teleportBorder(sender, args);
|
||||
case "add" -> add(sender, args);
|
||||
case "expand" -> expand(sender, args);
|
||||
case "setsize" -> setSize(sender, args);
|
||||
case "pause" -> pause(sender, args);
|
||||
case "resume" -> resume(sender, args);
|
||||
case "reset" -> reset(sender, args);
|
||||
case "gui" -> gui(sender);
|
||||
case "save" -> save(sender);
|
||||
default -> plugin.messageManager().sendList(sender, "commands.help", Map.of());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void add(CommandSender sender, String[] args) {
|
||||
if (args.length >= 4
|
||||
&& args[1].equalsIgnoreCase("legacy")
|
||||
&& (args[2].equalsIgnoreCase("users") || args[2].equalsIgnoreCase("user"))
|
||||
&& args[3].equalsIgnoreCase("scan")) {
|
||||
scanLegacyUsers(sender, args.length >= 5 ? args[4] : null);
|
||||
return;
|
||||
}
|
||||
plugin.messageManager().sendList(sender, "commands.help", Map.of());
|
||||
}
|
||||
|
||||
private void legacy(CommandSender sender, String[] args) {
|
||||
if (args.length < 2) {
|
||||
plugin.messageManager().sendList(sender, "commands.help", Map.of());
|
||||
return;
|
||||
}
|
||||
|
||||
switch (args[1].toLowerCase(Locale.ROOT)) {
|
||||
case "scan" -> scanLegacyUsers(sender, args.length >= 3 ? args[2] : null);
|
||||
case "list" -> listLegacyUsers(sender);
|
||||
case "add" -> addLegacyUser(sender, args);
|
||||
case "remove" -> removeLegacyUser(sender, args);
|
||||
default -> plugin.messageManager().sendList(sender, "commands.help", Map.of());
|
||||
}
|
||||
}
|
||||
|
||||
private void scanLegacyUsers(CommandSender sender, String rawMinimum) {
|
||||
if (!has(sender, "dirtsmp.legacy")) {
|
||||
plugin.messageManager().send(sender, "commands.no-permission");
|
||||
return;
|
||||
}
|
||||
|
||||
long minimumMillis = rawMinimum == null || rawMinimum.isBlank()
|
||||
? plugin.configManager().legacyUsersMinimumPlaytimeMillis()
|
||||
: TimeUtil.parseDurationMillis(rawMinimum, plugin.configManager().legacyUsersMinimumPlaytimeMillis());
|
||||
long minimumTicks = Math.max(1L, minimumMillis / 50L);
|
||||
|
||||
int scanned = 0;
|
||||
int matched = 0;
|
||||
int added = 0;
|
||||
for (OfflinePlayer player : Bukkit.getOfflinePlayers()) {
|
||||
if (!player.hasPlayedBefore()) {
|
||||
continue;
|
||||
}
|
||||
scanned++;
|
||||
long playtimeTicks = playtimeTicks(player);
|
||||
if (playtimeTicks < minimumTicks) {
|
||||
continue;
|
||||
}
|
||||
matched++;
|
||||
if (plugin.stateManager().addLegacyUser(player.getUniqueId(), player.getName(), playtimeTicks, "scan:" + TimeUtil.formatDuration(minimumMillis))) {
|
||||
added++;
|
||||
}
|
||||
}
|
||||
|
||||
plugin.stateManager().saveQuietly();
|
||||
plugin.messageManager().send(sender, "commands.legacy-scan", Map.of(
|
||||
"scanned", String.valueOf(scanned),
|
||||
"matched", String.valueOf(matched),
|
||||
"added", String.valueOf(added),
|
||||
"total", String.valueOf(plugin.stateManager().legacyUsers().size()),
|
||||
"minimum", TimeUtil.formatDuration(minimumMillis)
|
||||
));
|
||||
}
|
||||
|
||||
private void listLegacyUsers(CommandSender sender) {
|
||||
if (!has(sender, "dirtsmp.legacy")) {
|
||||
plugin.messageManager().send(sender, "commands.no-permission");
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.messageManager().send(sender, "commands.legacy-list-header", Map.of(
|
||||
"count", String.valueOf(plugin.stateManager().legacyUsers().size())
|
||||
));
|
||||
plugin.stateManager().legacyUsers().entrySet().stream()
|
||||
.sorted(Comparator.comparing(entry -> entry.getValue().name().toLowerCase(Locale.ROOT)))
|
||||
.limit(20)
|
||||
.forEach(entry -> plugin.messageManager().send(sender, "commands.legacy-list-line", Map.of(
|
||||
"name", entry.getValue().name(),
|
||||
"uuid", entry.getKey().toString(),
|
||||
"playtime", TimeUtil.formatDuration(entry.getValue().playtimeTicks() * 50L),
|
||||
"source", entry.getValue().source()
|
||||
)));
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private void addLegacyUser(CommandSender sender, String[] args) {
|
||||
if (!has(sender, "dirtsmp.legacy")) {
|
||||
plugin.messageManager().send(sender, "commands.no-permission");
|
||||
return;
|
||||
}
|
||||
if (args.length < 3) {
|
||||
plugin.messageManager().sendList(sender, "commands.help", Map.of());
|
||||
return;
|
||||
}
|
||||
|
||||
OfflinePlayer player = Bukkit.getOfflinePlayer(args[2]);
|
||||
long playtimeTicks = playtimeTicks(player);
|
||||
boolean added = plugin.stateManager().addLegacyUser(player.getUniqueId(), player.getName(), playtimeTicks, "manual:" + sender.getName());
|
||||
plugin.stateManager().saveQuietly();
|
||||
plugin.messageManager().send(sender, "commands.legacy-add", Map.of(
|
||||
"name", player.getName() == null ? args[2] : player.getName(),
|
||||
"status", added ? "added" : "updated"
|
||||
));
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private void removeLegacyUser(CommandSender sender, String[] args) {
|
||||
if (!has(sender, "dirtsmp.legacy")) {
|
||||
plugin.messageManager().send(sender, "commands.no-permission");
|
||||
return;
|
||||
}
|
||||
if (args.length < 3) {
|
||||
plugin.messageManager().sendList(sender, "commands.help", Map.of());
|
||||
return;
|
||||
}
|
||||
|
||||
UUID uuid = parseUuid(args[2]);
|
||||
OfflinePlayer player = uuid == null ? Bukkit.getOfflinePlayer(args[2]) : Bukkit.getOfflinePlayer(uuid);
|
||||
boolean removed = plugin.stateManager().removeLegacyUser(player.getUniqueId());
|
||||
plugin.stateManager().saveQuietly();
|
||||
plugin.messageManager().send(sender, "commands.legacy-remove", Map.of(
|
||||
"name", player.getName() == null ? args[2] : player.getName(),
|
||||
"status", removed ? "removed" : "not listed"
|
||||
));
|
||||
}
|
||||
|
||||
private long playtimeTicks(OfflinePlayer player) {
|
||||
try {
|
||||
return Math.max(0L, player.getStatistic(Statistic.PLAY_ONE_MINUTE));
|
||||
} catch (RuntimeException ex) {
|
||||
return 0L;
|
||||
}
|
||||
}
|
||||
|
||||
private UUID parseUuid(String value) {
|
||||
try {
|
||||
return UUID.fromString(value);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void reload(CommandSender sender) {
|
||||
if (!has(sender, "dirtsmp.reload")) {
|
||||
plugin.messageManager().send(sender, "commands.no-permission");
|
||||
return;
|
||||
}
|
||||
plugin.reloadDirtSMP();
|
||||
plugin.messageManager().send(sender, "commands.reload");
|
||||
}
|
||||
|
||||
private void save(CommandSender sender) {
|
||||
if (!has(sender, "dirtsmp.admin")) {
|
||||
plugin.messageManager().send(sender, "commands.no-permission");
|
||||
return;
|
||||
}
|
||||
plugin.saveAll();
|
||||
plugin.messageManager().send(sender, "commands.saved");
|
||||
}
|
||||
|
||||
private void status(CommandSender sender, String[] args) {
|
||||
if (!has(sender, "dirtsmp.status")) {
|
||||
plugin.messageManager().send(sender, "commands.no-permission");
|
||||
return;
|
||||
}
|
||||
if (args.length >= 2) {
|
||||
WorldRule rule = rule(sender, args[1]);
|
||||
if (rule != null) {
|
||||
plugin.messageManager().send(sender, "commands.status-line", placeholders(rule));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.messageManager().send(sender, "commands.status-header", Map.of("count", String.valueOf(plugin.configManager().worlds().size())));
|
||||
for (WorldRule rule : plugin.configManager().worlds().values()) {
|
||||
plugin.messageManager().send(sender, "commands.status-line", placeholders(rule));
|
||||
}
|
||||
}
|
||||
|
||||
private void next(CommandSender sender, String[] args) {
|
||||
if (!has(sender, "dirtsmp.status")) {
|
||||
plugin.messageManager().send(sender, "commands.no-permission");
|
||||
return;
|
||||
}
|
||||
if (args.length >= 2) {
|
||||
WorldRule rule = rule(sender, args[1]);
|
||||
if (rule != null) {
|
||||
plugin.messageManager().send(sender, "commands.next-line", placeholders(rule));
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (WorldRule rule : plugin.configManager().worlds().values()) {
|
||||
plugin.messageManager().send(sender, "commands.next-line", placeholders(rule));
|
||||
}
|
||||
}
|
||||
|
||||
private void legacyCheck(CommandSender sender, String[] args) {
|
||||
if (!has(sender, "dirtsmp.status")) {
|
||||
plugin.messageManager().send(sender, "commands.no-permission");
|
||||
return;
|
||||
}
|
||||
if (args.length < 2) {
|
||||
plugin.messageManager().sendList(sender, "commands.help", Map.of());
|
||||
return;
|
||||
}
|
||||
WorldRule rule = rule(sender, args[1]);
|
||||
if (rule == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
LegacyAccessReport report = plugin.borderManager().legacyAccessReport(rule);
|
||||
double recommended = Math.max(rule.initialSize(), report.requiredSize());
|
||||
plugin.messageManager().send(sender, "commands.legacy-check", Map.of(
|
||||
"world", rule.worldName(),
|
||||
"locations", String.valueOf(report.includedLocations()),
|
||||
"required_size", TimeUtil.formatSize(report.requiredSize()),
|
||||
"recommended_size", TimeUtil.formatSize(recommended),
|
||||
"initial_size", TimeUtil.formatSize(rule.initialSize()),
|
||||
"farthest", report.farthestLocationName()
|
||||
));
|
||||
}
|
||||
|
||||
private void teleportBorder(CommandSender sender, String[] args) {
|
||||
if (!has(sender, "dirtsmp.tpborder")) {
|
||||
plugin.messageManager().send(sender, "commands.no-permission");
|
||||
return;
|
||||
}
|
||||
if (!(sender instanceof Player player)) {
|
||||
plugin.messageManager().send(sender, "commands.player-only");
|
||||
return;
|
||||
}
|
||||
if (args.length < 2) {
|
||||
plugin.messageManager().sendList(sender, "commands.help", Map.of());
|
||||
return;
|
||||
}
|
||||
|
||||
WorldRule rule = rule(sender, args[1]);
|
||||
if (rule == null) {
|
||||
return;
|
||||
}
|
||||
World world = Bukkit.getWorld(rule.worldName());
|
||||
if (world == null) {
|
||||
plugin.messageManager().send(sender, "commands.tp-border-failed", Map.of(
|
||||
"world", rule.worldName(),
|
||||
"reason", "world is not loaded"
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
String side = args.length >= 3 ? args[2].toLowerCase(Locale.ROOT) : nearestSide(player, rule);
|
||||
if (!BORDER_SIDES.contains(side)) {
|
||||
plugin.messageManager().send(sender, "commands.tp-border-failed", Map.of(
|
||||
"world", rule.worldName(),
|
||||
"reason", "side must be north, south, east, or west"
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
Location destination = borderPreviewLocation(player, world, rule, side);
|
||||
destination.setY(safeY(world, destination));
|
||||
player.teleport(destination);
|
||||
plugin.messageManager().send(player, "commands.tp-border", Map.of(
|
||||
"world", rule.worldName(),
|
||||
"side", side,
|
||||
"size", TimeUtil.formatSize(currentSize(rule))
|
||||
));
|
||||
}
|
||||
|
||||
private void expand(CommandSender sender, String[] args) {
|
||||
if (!has(sender, "dirtsmp.expand")) {
|
||||
plugin.messageManager().send(sender, "commands.no-permission");
|
||||
return;
|
||||
}
|
||||
if (args.length < 2) {
|
||||
plugin.messageManager().sendList(sender, "commands.help", Map.of());
|
||||
return;
|
||||
}
|
||||
WorldRule rule = rule(sender, args[1]);
|
||||
if (rule == null) {
|
||||
return;
|
||||
}
|
||||
ExpansionResult result = plugin.borderManager().expand(rule, ExpansionReason.MANUAL, sender.getName(), true);
|
||||
if (result.success()) {
|
||||
plugin.messageManager().send(sender, "commands.expanded", Map.of(
|
||||
"world", rule.worldName(),
|
||||
"old_size", TimeUtil.formatSize(result.oldSize()),
|
||||
"new_size", TimeUtil.formatSize(result.newSize())
|
||||
));
|
||||
} else {
|
||||
plugin.messageManager().send(sender, "commands.expand-failed", Map.of("world", rule.worldName(), "reason", result.reason()));
|
||||
}
|
||||
}
|
||||
|
||||
private void setSize(CommandSender sender, String[] args) {
|
||||
if (!has(sender, "dirtsmp.setsize")) {
|
||||
plugin.messageManager().send(sender, "commands.no-permission");
|
||||
return;
|
||||
}
|
||||
if (args.length < 3) {
|
||||
plugin.messageManager().sendList(sender, "commands.help", Map.of());
|
||||
return;
|
||||
}
|
||||
WorldRule rule = rule(sender, args[1]);
|
||||
if (rule == null) {
|
||||
return;
|
||||
}
|
||||
double size;
|
||||
try {
|
||||
size = Double.parseDouble(args[2].replace(",", ""));
|
||||
} catch (NumberFormatException ex) {
|
||||
plugin.messageManager().send(sender, "commands.invalid-number");
|
||||
return;
|
||||
}
|
||||
boolean force = args.length >= 4 && args[3].equalsIgnoreCase("force");
|
||||
ExpansionResult result = plugin.borderManager().setSize(rule, size, sender.getName(), force);
|
||||
if (result.success()) {
|
||||
plugin.messageManager().send(sender, "commands.set-size", Map.of("world", rule.worldName(), "new_size", TimeUtil.formatSize(result.newSize())));
|
||||
} else {
|
||||
plugin.messageManager().send(sender, "commands.set-size-failed", Map.of("world", rule.worldName(), "reason", result.reason()));
|
||||
}
|
||||
}
|
||||
|
||||
private void pause(CommandSender sender, String[] args) {
|
||||
if (!has(sender, "dirtsmp.pause")) {
|
||||
plugin.messageManager().send(sender, "commands.no-permission");
|
||||
return;
|
||||
}
|
||||
WorldRule rule = args.length >= 2 ? rule(sender, args[1]) : null;
|
||||
if (rule == null) {
|
||||
return;
|
||||
}
|
||||
plugin.borderManager().pause(rule);
|
||||
plugin.messageManager().send(sender, "commands.paused", Map.of("world", rule.worldName()));
|
||||
}
|
||||
|
||||
private void resume(CommandSender sender, String[] args) {
|
||||
if (!has(sender, "dirtsmp.resume")) {
|
||||
plugin.messageManager().send(sender, "commands.no-permission");
|
||||
return;
|
||||
}
|
||||
WorldRule rule = args.length >= 2 ? rule(sender, args[1]) : null;
|
||||
if (rule == null) {
|
||||
return;
|
||||
}
|
||||
plugin.borderManager().resume(rule);
|
||||
plugin.messageManager().send(sender, "commands.resumed", Map.of("world", rule.worldName()));
|
||||
}
|
||||
|
||||
private void reset(CommandSender sender, String[] args) {
|
||||
if (!has(sender, "dirtsmp.reset")) {
|
||||
plugin.messageManager().send(sender, "commands.no-permission");
|
||||
return;
|
||||
}
|
||||
WorldRule rule = args.length >= 2 ? rule(sender, args[1]) : null;
|
||||
if (rule == null) {
|
||||
return;
|
||||
}
|
||||
boolean force = args.length >= 3 && args[2].equalsIgnoreCase("force");
|
||||
ExpansionResult result = plugin.borderManager().reset(rule, sender.getName(), force);
|
||||
if (result.success()) {
|
||||
plugin.messageManager().send(sender, "commands.reset", Map.of("world", rule.worldName()));
|
||||
} else {
|
||||
plugin.messageManager().send(sender, "commands.set-size-failed", Map.of("world", rule.worldName(), "reason", result.reason()));
|
||||
}
|
||||
}
|
||||
|
||||
private void gui(CommandSender sender) {
|
||||
if (!has(sender, "dirtsmp.gui")) {
|
||||
plugin.messageManager().send(sender, "commands.no-permission");
|
||||
return;
|
||||
}
|
||||
if (!(sender instanceof Player player)) {
|
||||
plugin.messageManager().send(sender, "commands.player-only");
|
||||
return;
|
||||
}
|
||||
if (!plugin.configManager().guiEnabled()) {
|
||||
plugin.messageManager().send(sender, "commands.gui-disabled");
|
||||
return;
|
||||
}
|
||||
plugin.adminGuiManager().open(player);
|
||||
}
|
||||
|
||||
private WorldRule rule(CommandSender sender, String key) {
|
||||
WorldRule rule = plugin.configManager().world(key);
|
||||
if (rule == null) {
|
||||
plugin.messageManager().send(sender, "commands.unknown-world", Map.of("world", key));
|
||||
}
|
||||
return rule;
|
||||
}
|
||||
|
||||
private Map<String, String> placeholders(WorldRule rule) {
|
||||
WorldProgress progress = plugin.stateManager().progress(rule.key());
|
||||
OptionalDouble nextSize = plugin.borderManager().nextSize(rule, progress);
|
||||
PhaseDefinition phase = plugin.borderManager().currentPhase(rule, progress);
|
||||
String nextTime = "manual";
|
||||
if (rule.triggers().mode().usesTime() && progress.lastExpansionAt() > 0L) {
|
||||
long next = progress.lastExpansionAt() + rule.triggers().timeIntervalMillis();
|
||||
nextTime = next <= System.currentTimeMillis() ? "due" : TimeUtil.formatDuration(next - System.currentTimeMillis());
|
||||
}
|
||||
return Map.ofEntries(
|
||||
Map.entry("key", rule.key()),
|
||||
Map.entry("world", rule.worldName()),
|
||||
Map.entry("enabled", String.valueOf(rule.enabled())),
|
||||
Map.entry("size", TimeUtil.formatSize(plugin.borderManager().displaySize(rule, progress))),
|
||||
Map.entry("next_size", nextSize.isPresent() ? TimeUtil.formatSize(nextSize.getAsDouble()) : "max"),
|
||||
Map.entry("border_mode", rule.borderMode().name()),
|
||||
Map.entry("phase", phase == null ? "none" : phase.name()),
|
||||
Map.entry("paused", String.valueOf(progress.paused())),
|
||||
Map.entry("trigger", plugin.triggerEvaluator().describeTrigger(rule, progress)),
|
||||
Map.entry("next_time", nextTime),
|
||||
Map.entry("expansion_count", String.valueOf(progress.expansionCount())),
|
||||
Map.entry("unique_progress", plugin.playerStatsManager().uniquePlayerCount() + "/" + (progress.uniquePlayersAtLastExpansion() + rule.triggers().uniquePlayersEvery()))
|
||||
);
|
||||
}
|
||||
|
||||
private Location borderPreviewLocation(Player player, World world, WorldRule rule, String side) {
|
||||
double half = currentSize(rule) / 2.0;
|
||||
double inset = Math.max(3.0, rule.softBorder().insideBuffer() + 1.0);
|
||||
double x = clamp(player.getLocation().getX(), rule.centerX() - half + inset, rule.centerX() + half - inset);
|
||||
double z = clamp(player.getLocation().getZ(), rule.centerZ() - half + inset, rule.centerZ() + half - inset);
|
||||
|
||||
switch (side) {
|
||||
case "north" -> z = rule.centerZ() - half + inset;
|
||||
case "south" -> z = rule.centerZ() + half - inset;
|
||||
case "east" -> x = rule.centerX() + half - inset;
|
||||
case "west" -> x = rule.centerX() - half + inset;
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
return new Location(world, x, player.getLocation().getY(), z, player.getLocation().getYaw(), player.getLocation().getPitch());
|
||||
}
|
||||
|
||||
private String nearestSide(Player player, WorldRule rule) {
|
||||
double half = currentSize(rule) / 2.0;
|
||||
double north = Math.abs(player.getLocation().getZ() - (rule.centerZ() - half));
|
||||
double south = Math.abs(player.getLocation().getZ() - (rule.centerZ() + half));
|
||||
double east = Math.abs(player.getLocation().getX() - (rule.centerX() + half));
|
||||
double west = Math.abs(player.getLocation().getX() - (rule.centerX() - half));
|
||||
double min = Math.min(Math.min(north, south), Math.min(east, west));
|
||||
if (min == north) {
|
||||
return "north";
|
||||
}
|
||||
if (min == south) {
|
||||
return "south";
|
||||
}
|
||||
return min == east ? "east" : "west";
|
||||
}
|
||||
|
||||
private double currentSize(WorldRule rule) {
|
||||
WorldProgress progress = plugin.stateManager().progress(rule.key());
|
||||
return progress.currentSize() > 0.0 ? progress.currentSize() : rule.initialSize();
|
||||
}
|
||||
|
||||
private double safeY(World world, Location location) {
|
||||
int x = location.getBlockX();
|
||||
int z = location.getBlockZ();
|
||||
int highest = world.getHighestBlockYAt(x, z);
|
||||
return Math.max(world.getMinHeight() + 1.0, Math.min(world.getMaxHeight() - 2.0, highest + 1.0));
|
||||
}
|
||||
|
||||
private double clamp(double value, double min, double max) {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
private boolean has(CommandSender sender, String permission) {
|
||||
return sender.hasPermission("dirtsmp.admin") || sender.hasPermission(permission);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
|
||||
if (args.length == 1) {
|
||||
return filter(SUBCOMMANDS, args[0]);
|
||||
}
|
||||
|
||||
String sub = args[0].toLowerCase(Locale.ROOT);
|
||||
if (args.length == 2 && List.of("status", "next", "legacycheck", "tpborder", "tpborderside", "expand", "setsize", "pause", "resume", "reset").contains(sub)) {
|
||||
return filter(new ArrayList<>(plugin.configManager().worlds().keySet()), args[1]);
|
||||
}
|
||||
if (args.length == 3 && List.of("tpborder", "tpborderside").contains(sub)) {
|
||||
return filter(BORDER_SIDES, args[2]);
|
||||
}
|
||||
if (sub.equals("legacy")) {
|
||||
if (args.length == 2) {
|
||||
return filter(List.of("scan", "list", "add", "remove"), args[1]);
|
||||
}
|
||||
if (args.length == 3 && args[1].equalsIgnoreCase("scan")) {
|
||||
return filter(List.of("1h", "30m", "2h"), args[2]);
|
||||
}
|
||||
if (args.length == 3 && List.of("add", "remove").contains(args[1].toLowerCase(Locale.ROOT))) {
|
||||
return filter(Bukkit.getOnlinePlayers().stream().map(Player::getName).toList(), args[2]);
|
||||
}
|
||||
}
|
||||
if (sub.equals("add")) {
|
||||
if (args.length == 2) {
|
||||
return filter(List.of("legacy"), args[1]);
|
||||
}
|
||||
if (args.length == 3 && args[1].equalsIgnoreCase("legacy")) {
|
||||
return filter(List.of("users"), args[2]);
|
||||
}
|
||||
if (args.length == 4 && args[1].equalsIgnoreCase("legacy") && args[2].equalsIgnoreCase("users")) {
|
||||
return filter(List.of("scan"), args[3]);
|
||||
}
|
||||
if (args.length == 5 && args[1].equalsIgnoreCase("legacy") && args[2].equalsIgnoreCase("users") && args[3].equalsIgnoreCase("scan")) {
|
||||
return filter(List.of("1h", "30m", "2h"), args[4]);
|
||||
}
|
||||
}
|
||||
if (args.length == 4 && sub.equals("setsize")) {
|
||||
return filter(List.of("force"), args[3]);
|
||||
}
|
||||
if (args.length == 3 && sub.equals("reset")) {
|
||||
return filter(List.of("force"), args[2]);
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
private List<String> filter(List<String> values, String prefix) {
|
||||
String lower = prefix.toLowerCase(Locale.ROOT);
|
||||
return values.stream()
|
||||
.filter(value -> value.toLowerCase(Locale.ROOT).startsWith(lower))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,482 @@
|
||||
package com.dirtsmp.dirtsmp.config;
|
||||
|
||||
import com.dirtsmp.dirtsmp.DirtSMPPlugin;
|
||||
import com.dirtsmp.dirtsmp.model.BorderMode;
|
||||
import com.dirtsmp.dirtsmp.model.CatchUpMode;
|
||||
import com.dirtsmp.dirtsmp.model.GrowthMode;
|
||||
import com.dirtsmp.dirtsmp.model.PhaseDefinition;
|
||||
import com.dirtsmp.dirtsmp.model.TriggerMode;
|
||||
import com.dirtsmp.dirtsmp.model.TriggerSettings;
|
||||
import com.dirtsmp.dirtsmp.model.WorldRule;
|
||||
import com.dirtsmp.dirtsmp.service.TimeUtil;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
|
||||
public final class ConfigManager {
|
||||
private final DirtSMPPlugin plugin;
|
||||
private FileConfiguration config;
|
||||
private YamlConfiguration messages;
|
||||
private Map<String, WorldRule> worlds = new LinkedHashMap<>();
|
||||
private WorldRule.MilestoneRewardSettings globalMilestoneRewards = WorldRule.MilestoneRewardSettings.empty();
|
||||
|
||||
public ConfigManager(DirtSMPPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public void load() {
|
||||
plugin.getDataFolder().mkdirs();
|
||||
plugin.saveDefaultConfig();
|
||||
saveBundledResource("messages.yml");
|
||||
|
||||
plugin.reloadConfig();
|
||||
config = plugin.getConfig();
|
||||
messages = YamlConfiguration.loadConfiguration(new File(plugin.getDataFolder(), "messages.yml"));
|
||||
globalMilestoneRewards = readMilestoneRewards(config.getConfigurationSection("milestone-rewards"));
|
||||
worlds = loadWorldRules();
|
||||
}
|
||||
|
||||
private void saveBundledResource(String name) {
|
||||
File target = new File(plugin.getDataFolder(), name);
|
||||
if (!target.exists()) {
|
||||
plugin.saveResource(name, false);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, WorldRule> loadWorldRules() {
|
||||
ConfigurationSection section = config.getConfigurationSection("worlds");
|
||||
if (section == null) {
|
||||
plugin.getLogger().warning("No worlds section found in config.yml. DirtSMP will not manage any borders.");
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
Map<String, WorldRule> loaded = new LinkedHashMap<>();
|
||||
for (String key : section.getKeys(false)) {
|
||||
ConfigurationSection worldSection = section.getConfigurationSection(key);
|
||||
if (worldSection == null) {
|
||||
continue;
|
||||
}
|
||||
WorldRule rule = readWorldRule(key, worldSection);
|
||||
loaded.put(key.toLowerCase(Locale.ROOT), rule);
|
||||
}
|
||||
return Collections.unmodifiableMap(loaded);
|
||||
}
|
||||
|
||||
private WorldRule readWorldRule(String key, ConfigurationSection section) {
|
||||
boolean enabled = section.getBoolean("enabled", true);
|
||||
String worldName = nonBlank(section.getString("world"), key);
|
||||
double centerX = section.getDouble("center-x", 0.0);
|
||||
double centerZ = section.getDouble("center-z", 0.0);
|
||||
double startingSize = positiveDouble(section, "starting-size", 1000.0);
|
||||
BorderMode borderMode = BorderMode.from(section.getString("border-mode", "WORLD_BORDER"));
|
||||
GrowthMode growthMode = GrowthMode.from(section.getString("growth-mode", "INCREMENTAL"));
|
||||
double growthAmount = positiveDouble(section, "growth-amount", 1000.0);
|
||||
double maxSize = positiveDouble(section, "max-size", 59_999_968.0);
|
||||
long transitionSeconds = Math.max(0L, section.getLong("transition-seconds", 60L));
|
||||
boolean manualOnly = section.getBoolean("manual-only", false);
|
||||
boolean importCurrentBorder = section.getBoolean("import-current-border", false);
|
||||
boolean noShrinkProtection = section.getBoolean("no-shrink-protection", true);
|
||||
boolean enforceMaxSize = section.getBoolean("enforce-max-size", true);
|
||||
|
||||
WorldRule.AnnouncementSettings announcements = readAnnouncements(section.getConfigurationSection("broadcasts"));
|
||||
WorldRule.ReminderSettings reminders = readReminders(section.getConfigurationSection("reminders"));
|
||||
TriggerSettings triggers = readTriggers(section.getConfigurationSection("triggers"));
|
||||
List<PhaseDefinition> phases = readPhases(section);
|
||||
WorldRule.MilestoneRewardSettings milestoneRewards = readMilestoneRewards(section.getConfigurationSection("milestone-rewards"));
|
||||
WorldRule.LegacyAccessSettings legacyAccess = readLegacyAccess(section.getConfigurationSection("legacy-access"));
|
||||
WorldRule.SoftBorderSettings softBorder = readSoftBorder(section.getConfigurationSection("soft-border"));
|
||||
|
||||
if (growthMode == GrowthMode.PHASE && phases.size() < 2) {
|
||||
plugin.getLogger().warning("World '" + key + "' is in PHASE mode but has fewer than two phases. It can initialize, but it has no scripted expansion path.");
|
||||
}
|
||||
|
||||
return new WorldRule(
|
||||
key.toLowerCase(Locale.ROOT),
|
||||
enabled,
|
||||
worldName,
|
||||
centerX,
|
||||
centerZ,
|
||||
startingSize,
|
||||
borderMode,
|
||||
growthMode,
|
||||
growthAmount,
|
||||
maxSize,
|
||||
transitionSeconds,
|
||||
manualOnly,
|
||||
importCurrentBorder,
|
||||
noShrinkProtection,
|
||||
enforceMaxSize,
|
||||
announcements,
|
||||
reminders,
|
||||
triggers,
|
||||
phases,
|
||||
milestoneRewards,
|
||||
legacyAccess,
|
||||
softBorder,
|
||||
stringList(section, "command-hooks.before-expansion"),
|
||||
stringList(section, "command-hooks.after-expansion")
|
||||
);
|
||||
}
|
||||
|
||||
private WorldRule.AnnouncementSettings readAnnouncements(ConfigurationSection section) {
|
||||
if (section == null) {
|
||||
return new WorldRule.AnnouncementSettings(true, false, "", "", "", "", "", 1.0f, 1.0f);
|
||||
}
|
||||
return new WorldRule.AnnouncementSettings(
|
||||
section.getBoolean("enabled", true),
|
||||
section.getBoolean("world-only", false),
|
||||
section.getString("message", ""),
|
||||
section.getString("title", ""),
|
||||
section.getString("subtitle", ""),
|
||||
section.getString("action-bar", ""),
|
||||
section.getString("sound", ""),
|
||||
(float) section.getDouble("volume", 1.0),
|
||||
(float) section.getDouble("pitch", 1.0)
|
||||
);
|
||||
}
|
||||
|
||||
private WorldRule.ReminderSettings readReminders(ConfigurationSection section) {
|
||||
if (section == null) {
|
||||
return new WorldRule.ReminderSettings(false, List.of(), "", "", "", 1.0f, 1.0f);
|
||||
}
|
||||
List<Long> reminders = new ArrayList<>();
|
||||
for (String raw : section.getStringList("before")) {
|
||||
long millis = TimeUtil.parseDurationMillis(raw, -1L);
|
||||
if (millis > 0L) {
|
||||
reminders.add(millis);
|
||||
} else {
|
||||
plugin.getLogger().warning("Ignoring invalid reminder duration '" + raw + "'.");
|
||||
}
|
||||
}
|
||||
reminders.sort(Collections.reverseOrder());
|
||||
return new WorldRule.ReminderSettings(
|
||||
section.getBoolean("enabled", false),
|
||||
List.copyOf(reminders),
|
||||
section.getString("message", ""),
|
||||
section.getString("action-bar", ""),
|
||||
section.getString("sound", ""),
|
||||
(float) section.getDouble("volume", 1.0),
|
||||
(float) section.getDouble("pitch", 1.0)
|
||||
);
|
||||
}
|
||||
|
||||
private TriggerSettings readTriggers(ConfigurationSection section) {
|
||||
if (section == null) {
|
||||
return new TriggerSettings(TriggerMode.MANUAL, 0L, 0, 0, 0, 0L, true, 0L, CatchUpMode.NONE);
|
||||
}
|
||||
|
||||
TriggerMode mode = TriggerMode.from(section.getString("mode", "MANUAL"));
|
||||
long interval = TimeUtil.parseDurationMillis(section.getString("time-interval", "7d"), 604_800_000L);
|
||||
long activeWindow = TimeUtil.parseDurationMillis(section.getString("active-window", "7d"), 604_800_000L);
|
||||
long cooldown = TimeUtil.parseDurationMillis(section.getString("player-trigger-cooldown", "12h"), 43_200_000L);
|
||||
|
||||
return new TriggerSettings(
|
||||
mode,
|
||||
interval,
|
||||
Math.max(0, section.getInt("unique-players-every", 0)),
|
||||
Math.max(0, section.getInt("online-players-threshold", 0)),
|
||||
Math.max(0, section.getInt("active-players-threshold", 0)),
|
||||
activeWindow,
|
||||
section.getBoolean("exclude-vanished", true),
|
||||
cooldown,
|
||||
CatchUpMode.from(section.getString("catch-up", "ONE"))
|
||||
);
|
||||
}
|
||||
|
||||
private List<PhaseDefinition> readPhases(ConfigurationSection section) {
|
||||
List<PhaseDefinition> phases = new ArrayList<>();
|
||||
int index = 0;
|
||||
for (Map<?, ?> phaseMap : section.getMapList("phases")) {
|
||||
Object sizeValue = phaseMap.get("size");
|
||||
double size = parseDouble(sizeValue, -1.0);
|
||||
if (size <= 0.0) {
|
||||
plugin.getLogger().warning("Ignoring phase with invalid size under world '" + section.getName() + "'.");
|
||||
continue;
|
||||
}
|
||||
phases.add(new PhaseDefinition(
|
||||
index,
|
||||
mapString(phaseMap, "name", "Phase " + index),
|
||||
size,
|
||||
mapString(phaseMap, "message", ""),
|
||||
mapString(phaseMap, "title", ""),
|
||||
mapString(phaseMap, "subtitle", "")
|
||||
));
|
||||
index++;
|
||||
}
|
||||
phases.sort((left, right) -> Integer.compare(left.index(), right.index()));
|
||||
return List.copyOf(phases);
|
||||
}
|
||||
|
||||
private WorldRule.MilestoneRewardSettings readMilestoneRewards(ConfigurationSection section) {
|
||||
if (section == null) {
|
||||
return WorldRule.MilestoneRewardSettings.empty();
|
||||
}
|
||||
|
||||
return new WorldRule.MilestoneRewardSettings(
|
||||
section.getBoolean("enabled", false),
|
||||
stringList(section, "every-expansion"),
|
||||
integerCommandMap(section.getConfigurationSection("by-expansion-count")),
|
||||
integerCommandMap(section.getConfigurationSection("by-phase-index")),
|
||||
stringCommandMap(section.getConfigurationSection("by-phase-name"))
|
||||
);
|
||||
}
|
||||
|
||||
private WorldRule.LegacyAccessSettings readLegacyAccess(ConfigurationSection section) {
|
||||
if (section == null) {
|
||||
return WorldRule.LegacyAccessSettings.disabled();
|
||||
}
|
||||
|
||||
List<WorldRule.LegacyLocation> locations = new ArrayList<>();
|
||||
for (Map<?, ?> locationMap : section.getMapList("locations")) {
|
||||
double x = parseDouble(locationMap.get("x"), Double.NaN);
|
||||
double z = parseDouble(locationMap.get("z"), Double.NaN);
|
||||
if (Double.isNaN(x) || Double.isNaN(z)) {
|
||||
plugin.getLogger().warning("Ignoring legacy-access location with invalid x/z under world '" + section.getParent().getName() + "'.");
|
||||
continue;
|
||||
}
|
||||
locations.add(new WorldRule.LegacyLocation(
|
||||
mapString(locationMap, "name", "legacy location"),
|
||||
x,
|
||||
z
|
||||
));
|
||||
}
|
||||
|
||||
return new WorldRule.LegacyAccessSettings(
|
||||
section.getBoolean("enabled", false),
|
||||
section.getBoolean("include-current-border", false),
|
||||
section.getBoolean("include-online-players", true),
|
||||
section.getBoolean("include-offline-last-locations", true),
|
||||
section.getBoolean("include-respawn-locations", true),
|
||||
section.getBoolean("player-locations-require-legacy-user", false),
|
||||
section.getBoolean("reconcile-on-startup", true),
|
||||
section.getBoolean("allow-start-above-max-size", true),
|
||||
Math.max(0.0, section.getDouble("padding", 1024.0)),
|
||||
List.copyOf(locations)
|
||||
);
|
||||
}
|
||||
|
||||
private WorldRule.SoftBorderSettings readSoftBorder(ConfigurationSection section) {
|
||||
WorldRule.SoftBorderSettings defaults = WorldRule.SoftBorderSettings.defaults();
|
||||
if (section == null) {
|
||||
return defaults;
|
||||
}
|
||||
|
||||
return new WorldRule.SoftBorderSettings(
|
||||
section.getBoolean("release-vanilla-border", defaults.releaseVanillaBorder()),
|
||||
positiveDouble(section, "vanilla-border-size", defaults.vanillaBorderSize()),
|
||||
section.getBoolean("ignore-creative", defaults.ignoreCreative()),
|
||||
section.getBoolean("ignore-spectator", defaults.ignoreSpectator()),
|
||||
nonBlank(section.getString("bypass-permission"), defaults.bypassPermission()),
|
||||
Math.max(0.1, section.getDouble("inside-buffer", defaults.insideBuffer())),
|
||||
Math.max(0.0, section.getDouble("bounce-strength", defaults.bounceStrength())),
|
||||
Math.max(0.0, section.getDouble("vertical-boost", defaults.verticalBoost())),
|
||||
section.getBoolean("protect-mounted-entities", defaults.protectMountedEntities()),
|
||||
Math.max(0.0, section.getDouble("mounted-bounce-strength", defaults.mountedBounceStrength())),
|
||||
Math.max(0.0, section.getDouble("mounted-vertical-boost", defaults.mountedVerticalBoost())),
|
||||
Math.max(0.0, section.getDouble("max-outside-distance-before-teleport", defaults.maxOutsideDistanceBeforeTeleport())),
|
||||
Math.max(0L, TimeUtil.parseDurationMillis(section.getString("cooldown", "900ms"), defaults.cooldownMillis())),
|
||||
section.getString("message", defaults.message()),
|
||||
section.getString("action-bar", defaults.actionBar()),
|
||||
section.getString("sound", defaults.sound()),
|
||||
(float) section.getDouble("volume", defaults.volume()),
|
||||
(float) section.getDouble("pitch", defaults.pitch()),
|
||||
section.getBoolean("particles.enabled", defaults.particlesEnabled()),
|
||||
section.getString("particles.particle", defaults.particle()),
|
||||
Math.max(0, section.getInt("particles.count", defaults.particleCount())),
|
||||
Math.max(0.0, section.getDouble("particles.offset", defaults.particleOffset())),
|
||||
Math.max(0.0, section.getDouble("particles.speed", defaults.particleSpeed())),
|
||||
Math.max(1L, section.getLong("particles.interval-ticks", defaults.particleIntervalTicks())),
|
||||
Math.max(8.0, section.getDouble("particles.view-distance", defaults.particleViewDistance())),
|
||||
Math.max(1.0, section.getDouble("particles.spacing", defaults.particleSpacing()))
|
||||
);
|
||||
}
|
||||
|
||||
private Map<Integer, List<String>> integerCommandMap(ConfigurationSection section) {
|
||||
if (section == null) {
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
Map<Integer, List<String>> commands = new LinkedHashMap<>();
|
||||
for (String key : section.getKeys(false)) {
|
||||
try {
|
||||
int milestone = Integer.parseInt(key);
|
||||
commands.put(milestone, List.copyOf(section.getStringList(key)));
|
||||
} catch (NumberFormatException ex) {
|
||||
plugin.getLogger().warning("Ignoring non-numeric milestone key '" + key + "' at " + section.getCurrentPath() + ".");
|
||||
}
|
||||
}
|
||||
return Collections.unmodifiableMap(commands);
|
||||
}
|
||||
|
||||
private Map<String, List<String>> stringCommandMap(ConfigurationSection section) {
|
||||
if (section == null) {
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
Map<String, List<String>> commands = new LinkedHashMap<>();
|
||||
for (String key : section.getKeys(false)) {
|
||||
commands.put(key.toLowerCase(Locale.ROOT), List.copyOf(section.getStringList(key)));
|
||||
}
|
||||
return Collections.unmodifiableMap(commands);
|
||||
}
|
||||
|
||||
private double positiveDouble(ConfigurationSection section, String path, double fallback) {
|
||||
double value = section.getDouble(path, fallback);
|
||||
if (value <= 0.0) {
|
||||
plugin.getLogger().warning("Invalid non-positive value at " + section.getCurrentPath() + "." + path + "; using " + fallback + ".");
|
||||
return fallback;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private double parseDouble(Object value, double fallback) {
|
||||
if (value instanceof Number number) {
|
||||
return number.doubleValue();
|
||||
}
|
||||
if (value == null) {
|
||||
return fallback;
|
||||
}
|
||||
try {
|
||||
return Double.parseDouble(String.valueOf(value));
|
||||
} catch (NumberFormatException ex) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
private String mapString(Map<?, ?> values, String key, String fallback) {
|
||||
Object value = values.get(key);
|
||||
return value == null ? fallback : String.valueOf(value);
|
||||
}
|
||||
|
||||
private String nonBlank(String value, String fallback) {
|
||||
return value == null || value.isBlank() ? fallback : value;
|
||||
}
|
||||
|
||||
private List<String> stringList(ConfigurationSection section, String path) {
|
||||
if (section == null) {
|
||||
return List.of();
|
||||
}
|
||||
return List.copyOf(section.getStringList(path));
|
||||
}
|
||||
|
||||
public FileConfiguration config() {
|
||||
return config;
|
||||
}
|
||||
|
||||
public YamlConfiguration messages() {
|
||||
return messages;
|
||||
}
|
||||
|
||||
public Map<String, WorldRule> worlds() {
|
||||
return worlds;
|
||||
}
|
||||
|
||||
public WorldRule world(String key) {
|
||||
if (key == null) {
|
||||
return null;
|
||||
}
|
||||
return worlds.get(key.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
public boolean dryRun() {
|
||||
return config.getBoolean("settings.dry-run", false);
|
||||
}
|
||||
|
||||
public boolean debug() {
|
||||
return config.getBoolean("settings.debug", false);
|
||||
}
|
||||
|
||||
public boolean applyBordersOnStartup() {
|
||||
return config.getBoolean("settings.apply-borders-on-startup", true);
|
||||
}
|
||||
|
||||
public long pollIntervalTicks() {
|
||||
return Math.max(1L, config.getLong("settings.poll-interval-seconds", 30L)) * 20L;
|
||||
}
|
||||
|
||||
public int maxCatchupExpansionsPerWorld() {
|
||||
return Math.max(1, config.getInt("settings.max-catchup-expansions-per-world", 20));
|
||||
}
|
||||
|
||||
public String stateFileName() {
|
||||
return config.getString("state.file-name", "state.yml");
|
||||
}
|
||||
|
||||
public long saveIntervalTicks() {
|
||||
return Math.max(20L, config.getLong("state.save-interval-seconds", 300L) * 20L);
|
||||
}
|
||||
|
||||
public long legacyUsersMinimumPlaytimeMillis() {
|
||||
return TimeUtil.parseDurationMillis(config.getString("legacy-users.minimum-playtime", "1h"), 3_600_000L);
|
||||
}
|
||||
|
||||
public boolean backupStateOnSave() {
|
||||
return config.getBoolean("state.backup-on-save", true);
|
||||
}
|
||||
|
||||
public int backupKeep() {
|
||||
return Math.max(0, config.getInt("state.backup-keep", 10));
|
||||
}
|
||||
|
||||
public boolean historyEnabled() {
|
||||
return config.getBoolean("history.enabled", true);
|
||||
}
|
||||
|
||||
public String historyFileName() {
|
||||
return config.getString("history.file-name", "history.log");
|
||||
}
|
||||
|
||||
public boolean guiEnabled() {
|
||||
return config.getBoolean("gui.enabled", true);
|
||||
}
|
||||
|
||||
public String guiTitle() {
|
||||
return config.getString("gui.title", "A2416&lᴅA2D1B&lɪA351F&lʀA3F24&lᴛA4A2A&lʙB6B35&lᴀ&#A8873F&lɢ&#D4AF37&lᴍ&#B9C63F&lᴄ &8| &#D4AF37&lBorders");
|
||||
}
|
||||
|
||||
public boolean hooksEnabled() {
|
||||
return config.getBoolean("command-hooks.enabled", true);
|
||||
}
|
||||
|
||||
public WorldRule.MilestoneRewardSettings globalMilestoneRewards() {
|
||||
return globalMilestoneRewards;
|
||||
}
|
||||
|
||||
public List<String> globalBeforeCommands() {
|
||||
return config.getStringList("command-hooks.before-expansion");
|
||||
}
|
||||
|
||||
public List<String> globalAfterCommands() {
|
||||
return config.getStringList("command-hooks.after-expansion");
|
||||
}
|
||||
|
||||
public boolean webhookEnabled() {
|
||||
return config.getBoolean("webhook.enabled", false);
|
||||
}
|
||||
|
||||
public String webhookUrl() {
|
||||
return config.getString("webhook.url", "");
|
||||
}
|
||||
|
||||
public String webhookContent() {
|
||||
return config.getString("webhook.content", "");
|
||||
}
|
||||
|
||||
public int webhookTimeoutSeconds() {
|
||||
return Math.max(1, config.getInt("webhook.timeout-seconds", 8));
|
||||
}
|
||||
|
||||
public void debug(String message) {
|
||||
if (debug()) {
|
||||
plugin.getLogger().log(Level.INFO, "[debug] " + message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
package com.dirtsmp.dirtsmp.config;
|
||||
|
||||
import com.dirtsmp.dirtsmp.model.ExpansionReason;
|
||||
import com.dirtsmp.dirtsmp.model.PhaseDefinition;
|
||||
import com.dirtsmp.dirtsmp.model.WorldRule;
|
||||
import com.dirtsmp.dirtsmp.service.TimeUtil;
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
|
||||
import net.kyori.adventure.title.Title;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.SoundCategory;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
public final class MessageManager {
|
||||
private final ConfigManager configManager;
|
||||
private final MiniMessage miniMessage = MiniMessage.miniMessage();
|
||||
private final LegacyComponentSerializer legacySerializer = LegacyComponentSerializer.builder()
|
||||
.character('&')
|
||||
.hexColors()
|
||||
.build();
|
||||
|
||||
public MessageManager(ConfigManager configManager) {
|
||||
this.configManager = configManager;
|
||||
}
|
||||
|
||||
public void send(CommandSender sender, String path) {
|
||||
send(sender, path, Map.of());
|
||||
}
|
||||
|
||||
public void send(CommandSender sender, String path, Map<String, String> placeholders) {
|
||||
String message = configManager.messages().getString(path, "");
|
||||
if (message == null || message.isBlank()) {
|
||||
return;
|
||||
}
|
||||
sender.sendMessage(component(message, placeholders));
|
||||
}
|
||||
|
||||
public void sendRaw(CommandSender sender, String raw, Map<String, String> placeholders) {
|
||||
if (raw == null || raw.isBlank()) {
|
||||
return;
|
||||
}
|
||||
sender.sendMessage(component(raw, placeholders));
|
||||
}
|
||||
|
||||
public void sendList(CommandSender sender, String path, Map<String, String> placeholders) {
|
||||
for (String line : configManager.messages().getStringList(path)) {
|
||||
sender.sendMessage(component(line, placeholders));
|
||||
}
|
||||
}
|
||||
|
||||
public Component component(String raw) {
|
||||
return component(raw, Map.of());
|
||||
}
|
||||
|
||||
public Component component(String raw, Map<String, String> placeholders) {
|
||||
String rendered = applyPlaceholders(raw, placeholders);
|
||||
try {
|
||||
if (usesLegacyFormatting(rendered)) {
|
||||
return legacySerializer.deserialize(rendered);
|
||||
}
|
||||
return miniMessage.deserialize(rendered);
|
||||
} catch (RuntimeException ex) {
|
||||
return Component.text(rendered.replaceAll("<[^>]*>", "").replaceAll("&[#0-9A-Fa-fK-Ok-orR]", ""));
|
||||
}
|
||||
}
|
||||
|
||||
public String applyPlaceholders(String raw, Map<String, String> placeholders) {
|
||||
String rendered = raw == null ? "" : raw;
|
||||
rendered = rendered.replace("{prefix}", configManager.messages().getString("prefix", ""));
|
||||
for (Map.Entry<String, String> entry : placeholders.entrySet()) {
|
||||
rendered = rendered.replace("{" + entry.getKey() + "}", entry.getValue() == null ? "" : entry.getValue());
|
||||
}
|
||||
return rendered;
|
||||
}
|
||||
|
||||
public void announceExpansion(WorldRule rule, double oldSize, double newSize, ExpansionReason reason, String actor, PhaseDefinition phase) {
|
||||
if (!rule.announcements().enabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, String> placeholders = basePlaceholders(rule, oldSize, newSize, reason, actor, phase);
|
||||
String message = firstNonBlank(
|
||||
phase == null ? "" : phase.message(),
|
||||
rule.announcements().message(),
|
||||
configManager.messages().getString("expansion.default-message", "")
|
||||
);
|
||||
String title = firstNonBlank(
|
||||
phase == null ? "" : phase.title(),
|
||||
rule.announcements().title(),
|
||||
configManager.messages().getString("expansion.default-title", "")
|
||||
);
|
||||
String subtitle = firstNonBlank(
|
||||
phase == null ? "" : phase.subtitle(),
|
||||
rule.announcements().subtitle(),
|
||||
configManager.messages().getString("expansion.default-subtitle", "")
|
||||
);
|
||||
String actionBar = firstNonBlank(
|
||||
rule.announcements().actionBar(),
|
||||
configManager.messages().getString("expansion.default-action-bar", "")
|
||||
);
|
||||
|
||||
List<? extends Player> recipients = Bukkit.getOnlinePlayers().stream()
|
||||
.filter(player -> !rule.announcements().worldOnly() || player.getWorld().getName().equals(rule.worldName()))
|
||||
.toList();
|
||||
|
||||
Component chat = component(message, placeholders);
|
||||
for (Player player : recipients) {
|
||||
player.sendMessage(chat);
|
||||
if (!title.isBlank() || !subtitle.isBlank()) {
|
||||
player.showTitle(Title.title(
|
||||
component(title, placeholders),
|
||||
component(subtitle, placeholders),
|
||||
Title.Times.times(Duration.ofMillis(500), Duration.ofSeconds(4), Duration.ofMillis(800))
|
||||
));
|
||||
}
|
||||
if (!actionBar.isBlank()) {
|
||||
player.sendActionBar(component(actionBar, placeholders));
|
||||
}
|
||||
playSound(player, rule.announcements().sound(), rule.announcements().volume(), rule.announcements().pitch());
|
||||
}
|
||||
Bukkit.getConsoleSender().sendMessage(chat);
|
||||
}
|
||||
|
||||
public void announceReminder(WorldRule rule, long timeLeftMillis) {
|
||||
if (!rule.reminders().enabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, String> placeholders = new HashMap<>();
|
||||
placeholders.put("key", rule.key());
|
||||
placeholders.put("world", rule.worldName());
|
||||
placeholders.put("time_left", TimeUtil.formatDuration(timeLeftMillis));
|
||||
|
||||
String message = firstNonBlank(
|
||||
rule.reminders().message(),
|
||||
configManager.messages().getString("reminders.default-message", "")
|
||||
);
|
||||
String actionBar = firstNonBlank(
|
||||
rule.reminders().actionBar(),
|
||||
configManager.messages().getString("reminders.default-action-bar", "")
|
||||
);
|
||||
|
||||
Component chat = component(message, placeholders);
|
||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||
player.sendMessage(chat);
|
||||
if (!actionBar.isBlank()) {
|
||||
player.sendActionBar(component(actionBar, placeholders));
|
||||
}
|
||||
playSound(player, rule.reminders().sound(), rule.reminders().volume(), rule.reminders().pitch());
|
||||
}
|
||||
Bukkit.getConsoleSender().sendMessage(chat);
|
||||
}
|
||||
|
||||
public Map<String, String> basePlaceholders(WorldRule rule, double oldSize, double newSize, ExpansionReason reason, String actor, PhaseDefinition phase) {
|
||||
Map<String, String> placeholders = new HashMap<>();
|
||||
placeholders.put("key", rule.key());
|
||||
placeholders.put("world", rule.worldName());
|
||||
placeholders.put("old_size", TimeUtil.formatSize(oldSize));
|
||||
placeholders.put("new_size", TimeUtil.formatSize(newSize));
|
||||
placeholders.put("max_size", TimeUtil.formatSize(rule.maxSize()));
|
||||
placeholders.put("reason", reason.name().toLowerCase(Locale.ROOT).replace('_', ' '));
|
||||
placeholders.put("actor", actor == null ? "console" : actor);
|
||||
placeholders.put("phase", phase == null ? "none" : String.valueOf(phase.index()));
|
||||
placeholders.put("phase_name", phase == null ? "none" : phase.name());
|
||||
return placeholders;
|
||||
}
|
||||
|
||||
public Map<String, String> withBasePlaceholders(WorldRule rule, double oldSize, double newSize, ExpansionReason reason, String actor, PhaseDefinition phase, Map<String, String> extra) {
|
||||
Map<String, String> placeholders = basePlaceholders(rule, oldSize, newSize, reason, actor, phase);
|
||||
placeholders.putAll(extra);
|
||||
return placeholders;
|
||||
}
|
||||
|
||||
@SuppressWarnings({"deprecation", "removal"})
|
||||
private void playSound(Player player, String soundName, float volume, float pitch) {
|
||||
if (soundName == null || soundName.isBlank()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Sound sound = Sound.valueOf(soundName.trim().toUpperCase(Locale.ROOT));
|
||||
player.playSound(player.getLocation(), sound, SoundCategory.MASTER, volume, pitch);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
// Invalid sounds are config mistakes; they are non-fatal and already visible in config.
|
||||
}
|
||||
}
|
||||
|
||||
private String firstNonBlank(String... values) {
|
||||
for (String value : values) {
|
||||
if (value != null && !value.isBlank()) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private boolean usesLegacyFormatting(String rendered) {
|
||||
return rendered.matches("(?s).*&(?:#[0-9A-Fa-f]{6}|[0-9A-Fa-fK-Ok-orR]).*");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
package com.dirtsmp.dirtsmp.gui;
|
||||
|
||||
import com.dirtsmp.dirtsmp.DirtSMPPlugin;
|
||||
import com.dirtsmp.dirtsmp.config.ConfigManager;
|
||||
import com.dirtsmp.dirtsmp.config.MessageManager;
|
||||
import com.dirtsmp.dirtsmp.model.ExpansionReason;
|
||||
import com.dirtsmp.dirtsmp.model.ExpansionResult;
|
||||
import com.dirtsmp.dirtsmp.model.PhaseDefinition;
|
||||
import com.dirtsmp.dirtsmp.model.WorldRule;
|
||||
import com.dirtsmp.dirtsmp.service.BorderManager;
|
||||
import com.dirtsmp.dirtsmp.service.TimeUtil;
|
||||
import com.dirtsmp.dirtsmp.service.TriggerEvaluator;
|
||||
import com.dirtsmp.dirtsmp.state.StateManager;
|
||||
import com.dirtsmp.dirtsmp.state.WorldProgress;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.OptionalDouble;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.InventoryHolder;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
|
||||
public final class AdminGuiManager implements Listener {
|
||||
private final DirtSMPPlugin plugin;
|
||||
private final ConfigManager configManager;
|
||||
private final MessageManager messageManager;
|
||||
private final StateManager stateManager;
|
||||
private final BorderManager borderManager;
|
||||
private final TriggerEvaluator triggerEvaluator;
|
||||
|
||||
public AdminGuiManager(DirtSMPPlugin plugin, ConfigManager configManager, MessageManager messageManager, StateManager stateManager, BorderManager borderManager, TriggerEvaluator triggerEvaluator) {
|
||||
this.plugin = plugin;
|
||||
this.configManager = configManager;
|
||||
this.messageManager = messageManager;
|
||||
this.stateManager = stateManager;
|
||||
this.borderManager = borderManager;
|
||||
this.triggerEvaluator = triggerEvaluator;
|
||||
}
|
||||
|
||||
public void open(Player player) {
|
||||
GuiHolder holder = new GuiHolder();
|
||||
Inventory inventory = Bukkit.createInventory(holder, 27, messageManager.component(configManager.guiTitle()));
|
||||
holder.inventory = inventory;
|
||||
|
||||
int[] slots = {10, 11, 12, 13, 14, 15, 16};
|
||||
int index = 0;
|
||||
for (WorldRule rule : configManager.worlds().values()) {
|
||||
if (index >= slots.length) {
|
||||
break;
|
||||
}
|
||||
int slot = slots[index++];
|
||||
inventory.setItem(slot, worldItem(rule));
|
||||
holder.worldBySlot.put(slot, rule.key());
|
||||
}
|
||||
|
||||
inventory.setItem(22, simpleItem(Material.COMPASS, configManager.messages().getString("gui.refresh", "&#B9C63FRefresh")));
|
||||
inventory.setItem(26, simpleItem(Material.BARRIER, configManager.messages().getString("gui.close", "&cClose")));
|
||||
player.openInventory(inventory);
|
||||
}
|
||||
|
||||
public void closeOpenInventories() {
|
||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||
if (player.getOpenInventory().getTopInventory().getHolder() instanceof GuiHolder) {
|
||||
player.closeInventory();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onClick(InventoryClickEvent event) {
|
||||
if (!(event.getInventory().getHolder() instanceof GuiHolder holder)) {
|
||||
return;
|
||||
}
|
||||
event.setCancelled(true);
|
||||
if (!(event.getWhoClicked() instanceof Player player)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int slot = event.getRawSlot();
|
||||
if (slot == 26) {
|
||||
player.closeInventory();
|
||||
return;
|
||||
}
|
||||
if (slot == 22) {
|
||||
open(player);
|
||||
return;
|
||||
}
|
||||
|
||||
String key = holder.worldBySlot.get(slot);
|
||||
if (key == null) {
|
||||
return;
|
||||
}
|
||||
WorldRule rule = configManager.world(key);
|
||||
if (rule == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.isLeftClick()) {
|
||||
if (!player.hasPermission("dirtsmp.admin") && !player.hasPermission("dirtsmp.expand")) {
|
||||
messageManager.send(player, "commands.no-permission");
|
||||
return;
|
||||
}
|
||||
ExpansionResult result = borderManager.expand(rule, ExpansionReason.MANUAL, player.getName(), true);
|
||||
if (!result.success()) {
|
||||
messageManager.send(player, "commands.expand-failed", Map.of("world", rule.worldName(), "reason", result.reason()));
|
||||
}
|
||||
open(player);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.isRightClick()) {
|
||||
WorldProgress progress = stateManager.progress(rule.key());
|
||||
if (progress.paused()) {
|
||||
if (!player.hasPermission("dirtsmp.admin") && !player.hasPermission("dirtsmp.resume")) {
|
||||
messageManager.send(player, "commands.no-permission");
|
||||
return;
|
||||
}
|
||||
borderManager.resume(rule);
|
||||
messageManager.send(player, "commands.resumed", Map.of("world", rule.worldName()));
|
||||
} else {
|
||||
if (!player.hasPermission("dirtsmp.admin") && !player.hasPermission("dirtsmp.pause")) {
|
||||
messageManager.send(player, "commands.no-permission");
|
||||
return;
|
||||
}
|
||||
borderManager.pause(rule);
|
||||
messageManager.send(player, "commands.paused", Map.of("world", rule.worldName()));
|
||||
}
|
||||
open(player);
|
||||
}
|
||||
}
|
||||
|
||||
private ItemStack worldItem(WorldRule rule) {
|
||||
WorldProgress progress = stateManager.progress(rule.key());
|
||||
Material material = materialFor(rule, progress);
|
||||
ItemStack item = new ItemStack(material);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
Map<String, String> placeholders = placeholders(rule, progress);
|
||||
meta.displayName(messageManager.component(configManager.messages().getString("gui.world-name", "&#D4AF37&l{key}"), placeholders));
|
||||
List<Component> lore = configManager.messages().getStringList("gui.world-lore").stream()
|
||||
.map(line -> messageManager.component(line, placeholders))
|
||||
.toList();
|
||||
meta.lore(lore);
|
||||
item.setItemMeta(meta);
|
||||
return item;
|
||||
}
|
||||
|
||||
private ItemStack simpleItem(Material material, String name) {
|
||||
ItemStack item = new ItemStack(material);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
meta.displayName(messageManager.component(name));
|
||||
item.setItemMeta(meta);
|
||||
return item;
|
||||
}
|
||||
|
||||
private Material materialFor(WorldRule rule, WorldProgress progress) {
|
||||
if (progress.paused()) {
|
||||
return Material.REDSTONE_BLOCK;
|
||||
}
|
||||
String worldName = rule.worldName().toLowerCase();
|
||||
if (worldName.contains("the_end") || worldName.contains("end")) {
|
||||
return Material.END_STONE;
|
||||
}
|
||||
if (worldName.contains("nether")) {
|
||||
return Material.NETHERRACK;
|
||||
}
|
||||
return Material.GRASS_BLOCK;
|
||||
}
|
||||
|
||||
private Map<String, String> placeholders(WorldRule rule, WorldProgress progress) {
|
||||
OptionalDouble next = borderManager.nextSize(rule, progress);
|
||||
PhaseDefinition phase = borderManager.currentPhase(rule, progress);
|
||||
return Map.of(
|
||||
"key", rule.key(),
|
||||
"world", rule.worldName(),
|
||||
"enabled", String.valueOf(rule.enabled()),
|
||||
"size", TimeUtil.formatSize(borderManager.displaySize(rule, progress)),
|
||||
"next_size", next.isPresent() ? TimeUtil.formatSize(next.getAsDouble()) : "max",
|
||||
"border_mode", rule.borderMode().name(),
|
||||
"trigger", triggerEvaluator.describeTrigger(rule, progress),
|
||||
"phase", phase == null ? "none" : phase.name(),
|
||||
"paused", String.valueOf(progress.paused())
|
||||
);
|
||||
}
|
||||
|
||||
private static final class GuiHolder implements InventoryHolder {
|
||||
private final Map<Integer, String> worldBySlot = new HashMap<>();
|
||||
private Inventory inventory;
|
||||
|
||||
@Override
|
||||
public Inventory getInventory() {
|
||||
return inventory;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package com.dirtsmp.dirtsmp.hook;
|
||||
|
||||
import com.dirtsmp.dirtsmp.DirtSMPPlugin;
|
||||
import com.dirtsmp.dirtsmp.model.PhaseDefinition;
|
||||
import com.dirtsmp.dirtsmp.model.WorldRule;
|
||||
import com.dirtsmp.dirtsmp.service.TimeUtil;
|
||||
import com.dirtsmp.dirtsmp.state.WorldProgress;
|
||||
import java.util.Locale;
|
||||
import java.util.OptionalDouble;
|
||||
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public final class DirtSMPPlaceholderExpansion extends PlaceholderExpansion {
|
||||
private final DirtSMPPlugin plugin;
|
||||
|
||||
public DirtSMPPlaceholderExpansion(DirtSMPPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String getIdentifier() {
|
||||
return "dirtsmp";
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String getAuthor() {
|
||||
return "DirtSMP";
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String getVersion() {
|
||||
return plugin.getDescription().getVersion();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean persist() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String onRequest(OfflinePlayer player, @NotNull String params) {
|
||||
String lower = params.toLowerCase(Locale.ROOT);
|
||||
if (lower.startsWith("current_size")) {
|
||||
WorldRule rule = ruleFor(lower, "current_size");
|
||||
return rule == null ? "" : TimeUtil.formatSize(plugin.borderManager().displaySize(rule, progress(rule)));
|
||||
}
|
||||
if (lower.startsWith("next_size")) {
|
||||
WorldRule rule = ruleFor(lower, "next_size");
|
||||
if (rule == null) {
|
||||
return "";
|
||||
}
|
||||
OptionalDouble next = plugin.borderManager().nextSize(rule, progress(rule));
|
||||
return next.isPresent() ? TimeUtil.formatSize(next.getAsDouble()) : "max";
|
||||
}
|
||||
if (lower.startsWith("next_expansion_time") || lower.startsWith("next_time")) {
|
||||
String prefix = lower.startsWith("next_expansion_time") ? "next_expansion_time" : "next_time";
|
||||
WorldRule rule = ruleFor(lower, prefix);
|
||||
if (rule == null) {
|
||||
return "";
|
||||
}
|
||||
WorldProgress progress = progress(rule);
|
||||
if (!rule.triggers().mode().usesTime() || progress.lastExpansionAt() <= 0L) {
|
||||
return "manual";
|
||||
}
|
||||
long next = progress.lastExpansionAt() + rule.triggers().timeIntervalMillis();
|
||||
return next <= System.currentTimeMillis() ? "due" : TimeUtil.formatDuration(next - System.currentTimeMillis());
|
||||
}
|
||||
if (lower.startsWith("phase")) {
|
||||
WorldRule rule = ruleFor(lower, "phase");
|
||||
if (rule == null) {
|
||||
return "";
|
||||
}
|
||||
PhaseDefinition phase = plugin.borderManager().currentPhase(rule, progress(rule));
|
||||
return phase == null ? "none" : phase.name();
|
||||
}
|
||||
if (lower.startsWith("expansion_count")) {
|
||||
WorldRule rule = ruleFor(lower, "expansion_count");
|
||||
return rule == null ? "" : String.valueOf(progress(rule).expansionCount());
|
||||
}
|
||||
if (lower.startsWith("unique_progress")) {
|
||||
WorldRule rule = ruleFor(lower, "unique_progress");
|
||||
if (rule == null) {
|
||||
return "";
|
||||
}
|
||||
WorldProgress progress = progress(rule);
|
||||
int needed = progress.uniquePlayersAtLastExpansion() + rule.triggers().uniquePlayersEvery();
|
||||
return plugin.playerStatsManager().uniquePlayerCount() + "/" + needed;
|
||||
}
|
||||
if (lower.startsWith("paused")) {
|
||||
WorldRule rule = ruleFor(lower, "paused");
|
||||
return rule == null ? "" : String.valueOf(progress(rule).paused());
|
||||
}
|
||||
if (lower.startsWith("border_mode")) {
|
||||
WorldRule rule = ruleFor(lower, "border_mode");
|
||||
return rule == null ? "" : rule.borderMode().name();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private WorldRule ruleFor(String params, String prefix) {
|
||||
if (params.equals(prefix)) {
|
||||
return plugin.configManager().worlds().values().stream().findFirst().orElse(null);
|
||||
}
|
||||
String key = params.substring(prefix.length());
|
||||
if (key.startsWith("_")) {
|
||||
key = key.substring(1);
|
||||
}
|
||||
return plugin.configManager().world(key);
|
||||
}
|
||||
|
||||
private WorldProgress progress(WorldRule rule) {
|
||||
return plugin.stateManager().progress(rule.key());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.dirtsmp.dirtsmp.hook;
|
||||
|
||||
import com.dirtsmp.dirtsmp.DirtSMPPlugin;
|
||||
|
||||
public final class PlaceholderHook {
|
||||
private final DirtSMPPlugin plugin;
|
||||
private DirtSMPPlaceholderExpansion expansion;
|
||||
|
||||
public PlaceholderHook(DirtSMPPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public void register() {
|
||||
expansion = new DirtSMPPlaceholderExpansion(plugin);
|
||||
if (expansion.register()) {
|
||||
plugin.getLogger().info("Registered PlaceholderAPI expansion.");
|
||||
}
|
||||
}
|
||||
|
||||
public void unregister() {
|
||||
if (expansion != null) {
|
||||
expansion.unregister();
|
||||
expansion = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.dirtsmp.dirtsmp.listener;
|
||||
|
||||
import com.dirtsmp.dirtsmp.DirtSMPPlugin;
|
||||
import com.dirtsmp.dirtsmp.config.ConfigManager;
|
||||
import com.dirtsmp.dirtsmp.model.WorldRule;
|
||||
import com.dirtsmp.dirtsmp.service.BorderManager;
|
||||
import com.dirtsmp.dirtsmp.state.PlayerStatsManager;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
import org.bukkit.event.world.WorldLoadEvent;
|
||||
|
||||
public final class PlayerListener implements Listener {
|
||||
private final DirtSMPPlugin plugin;
|
||||
private final PlayerStatsManager playerStatsManager;
|
||||
private final BorderManager borderManager;
|
||||
private final ConfigManager configManager;
|
||||
|
||||
public PlayerListener(DirtSMPPlugin plugin, PlayerStatsManager playerStatsManager, BorderManager borderManager, ConfigManager configManager) {
|
||||
this.plugin = plugin;
|
||||
this.playerStatsManager = playerStatsManager;
|
||||
this.borderManager = borderManager;
|
||||
this.configManager = configManager;
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onJoin(PlayerJoinEvent event) {
|
||||
playerStatsManager.markSeen(event.getPlayer());
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onQuit(PlayerQuitEvent event) {
|
||||
playerStatsManager.markSeen(event.getPlayer());
|
||||
playerStatsManager.saveQuietly();
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onWorldLoad(WorldLoadEvent event) {
|
||||
for (WorldRule rule : configManager.worlds().values()) {
|
||||
if (rule.enabled() && rule.worldName().equals(event.getWorld().getName())) {
|
||||
plugin.getLogger().info("Applying DirtSMP border settings to newly loaded world '" + rule.worldName() + "'.");
|
||||
borderManager.initializeRule(rule);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.dirtsmp.dirtsmp.model;
|
||||
|
||||
public enum BorderMode {
|
||||
WORLD_BORDER,
|
||||
SOFT_BORDER;
|
||||
|
||||
public static BorderMode from(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return WORLD_BORDER;
|
||||
}
|
||||
try {
|
||||
return BorderMode.valueOf(value.trim().toUpperCase());
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
return WORLD_BORDER;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.dirtsmp.dirtsmp.model;
|
||||
|
||||
public enum CatchUpMode {
|
||||
NONE,
|
||||
ONE,
|
||||
ALL;
|
||||
|
||||
public static CatchUpMode from(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return ONE;
|
||||
}
|
||||
try {
|
||||
return CatchUpMode.valueOf(value.trim().toUpperCase());
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
return ONE;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.dirtsmp.dirtsmp.model;
|
||||
|
||||
public enum ExpansionReason {
|
||||
TIME,
|
||||
UNIQUE_PLAYERS,
|
||||
ONLINE_PLAYERS,
|
||||
ACTIVE_PLAYERS,
|
||||
HYBRID,
|
||||
CATCH_UP,
|
||||
MANUAL,
|
||||
SET_SIZE,
|
||||
RESET;
|
||||
|
||||
public boolean automatic() {
|
||||
return this == TIME
|
||||
|| this == UNIQUE_PLAYERS
|
||||
|| this == ONLINE_PLAYERS
|
||||
|| this == ACTIVE_PLAYERS
|
||||
|| this == HYBRID
|
||||
|| this == CATCH_UP;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.dirtsmp.dirtsmp.model;
|
||||
|
||||
public record ExpansionResult(
|
||||
boolean success,
|
||||
String reason,
|
||||
double oldSize,
|
||||
double newSize,
|
||||
PhaseDefinition phase
|
||||
) {
|
||||
public static ExpansionResult success(double oldSize, double newSize, PhaseDefinition phase) {
|
||||
return new ExpansionResult(true, "", oldSize, newSize, phase);
|
||||
}
|
||||
|
||||
public static ExpansionResult failure(String reason) {
|
||||
return new ExpansionResult(false, reason, 0.0, 0.0, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.dirtsmp.dirtsmp.model;
|
||||
|
||||
public enum GrowthMode {
|
||||
INCREMENTAL,
|
||||
PHASE;
|
||||
|
||||
public static GrowthMode from(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return INCREMENTAL;
|
||||
}
|
||||
try {
|
||||
return GrowthMode.valueOf(value.trim().toUpperCase());
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
return INCREMENTAL;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.dirtsmp.dirtsmp.model;
|
||||
|
||||
public record LegacyAccessReport(
|
||||
double requiredSize,
|
||||
int includedLocations,
|
||||
String farthestLocationName
|
||||
) {
|
||||
public boolean hasLocations() {
|
||||
return includedLocations > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.dirtsmp.dirtsmp.model;
|
||||
|
||||
public record PhaseDefinition(
|
||||
int index,
|
||||
String name,
|
||||
double size,
|
||||
String message,
|
||||
String title,
|
||||
String subtitle
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.dirtsmp.dirtsmp.model;
|
||||
|
||||
public record TriggerDecision(
|
||||
boolean due,
|
||||
ExpansionReason reason,
|
||||
String description
|
||||
) {
|
||||
public static TriggerDecision none(String description) {
|
||||
return new TriggerDecision(false, ExpansionReason.MANUAL, description);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.dirtsmp.dirtsmp.model;
|
||||
|
||||
public enum TriggerMode {
|
||||
MANUAL,
|
||||
TIME,
|
||||
UNIQUE_PLAYERS,
|
||||
ONLINE_PLAYERS,
|
||||
ACTIVE_PLAYERS,
|
||||
TIME_OR_UNIQUE_PLAYERS,
|
||||
TIME_AND_UNIQUE_PLAYERS,
|
||||
TIME_OR_ONLINE_PLAYERS,
|
||||
TIME_AND_ONLINE_PLAYERS,
|
||||
TIME_OR_ACTIVE_PLAYERS,
|
||||
TIME_AND_ACTIVE_PLAYERS;
|
||||
|
||||
public static TriggerMode from(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return MANUAL;
|
||||
}
|
||||
try {
|
||||
return TriggerMode.valueOf(value.trim().toUpperCase());
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
return MANUAL;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean usesTime() {
|
||||
return this == TIME
|
||||
|| this == TIME_OR_UNIQUE_PLAYERS
|
||||
|| this == TIME_AND_UNIQUE_PLAYERS
|
||||
|| this == TIME_OR_ONLINE_PLAYERS
|
||||
|| this == TIME_AND_ONLINE_PLAYERS
|
||||
|| this == TIME_OR_ACTIVE_PLAYERS
|
||||
|| this == TIME_AND_ACTIVE_PLAYERS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.dirtsmp.dirtsmp.model;
|
||||
|
||||
public record TriggerSettings(
|
||||
TriggerMode mode,
|
||||
long timeIntervalMillis,
|
||||
int uniquePlayersEvery,
|
||||
int onlinePlayersThreshold,
|
||||
int activePlayersThreshold,
|
||||
long activeWindowMillis,
|
||||
boolean excludeVanished,
|
||||
long playerTriggerCooldownMillis,
|
||||
CatchUpMode catchUpMode
|
||||
) {
|
||||
public boolean manualOnly() {
|
||||
return mode == TriggerMode.MANUAL;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package com.dirtsmp.dirtsmp.model;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
public record WorldRule(
|
||||
String key,
|
||||
boolean enabled,
|
||||
String worldName,
|
||||
double centerX,
|
||||
double centerZ,
|
||||
double startingSize,
|
||||
BorderMode borderMode,
|
||||
GrowthMode growthMode,
|
||||
double growthAmount,
|
||||
double maxSize,
|
||||
long transitionSeconds,
|
||||
boolean manualOnly,
|
||||
boolean importCurrentBorder,
|
||||
boolean noShrinkProtection,
|
||||
boolean enforceMaxSize,
|
||||
AnnouncementSettings announcements,
|
||||
ReminderSettings reminders,
|
||||
TriggerSettings triggers,
|
||||
List<PhaseDefinition> phases,
|
||||
MilestoneRewardSettings milestoneRewards,
|
||||
LegacyAccessSettings legacyAccess,
|
||||
SoftBorderSettings softBorder,
|
||||
List<String> beforeCommands,
|
||||
List<String> afterCommands
|
||||
) {
|
||||
public double initialSize() {
|
||||
if (growthMode == GrowthMode.PHASE && !phases.isEmpty()) {
|
||||
return phases.getFirst().size();
|
||||
}
|
||||
return startingSize;
|
||||
}
|
||||
|
||||
public Optional<PhaseDefinition> phase(int index) {
|
||||
if (index < 0 || index >= phases.size()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(phases.get(index));
|
||||
}
|
||||
|
||||
public Optional<PhaseDefinition> nextPhase(int currentIndex) {
|
||||
return phase(currentIndex + 1);
|
||||
}
|
||||
|
||||
public int phaseIndexForSize(double size) {
|
||||
int index = phases.isEmpty() ? -1 : 0;
|
||||
for (PhaseDefinition phase : phases) {
|
||||
if (size + 0.0001 >= phase.size()) {
|
||||
index = phase.index();
|
||||
}
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
public record AnnouncementSettings(
|
||||
boolean enabled,
|
||||
boolean worldOnly,
|
||||
String message,
|
||||
String title,
|
||||
String subtitle,
|
||||
String actionBar,
|
||||
String sound,
|
||||
float volume,
|
||||
float pitch
|
||||
) {
|
||||
}
|
||||
|
||||
public record ReminderSettings(
|
||||
boolean enabled,
|
||||
List<Long> beforeMillis,
|
||||
String message,
|
||||
String actionBar,
|
||||
String sound,
|
||||
float volume,
|
||||
float pitch
|
||||
) {
|
||||
}
|
||||
|
||||
public record MilestoneRewardSettings(
|
||||
boolean enabled,
|
||||
List<String> everyExpansionCommands,
|
||||
Map<Integer, List<String>> expansionCountCommands,
|
||||
Map<Integer, List<String>> phaseIndexCommands,
|
||||
Map<String, List<String>> phaseNameCommands
|
||||
) {
|
||||
public static MilestoneRewardSettings empty() {
|
||||
return new MilestoneRewardSettings(false, List.of(), Map.of(), Map.of(), Map.of());
|
||||
}
|
||||
}
|
||||
|
||||
public record LegacyAccessSettings(
|
||||
boolean enabled,
|
||||
boolean includeCurrentBorder,
|
||||
boolean includeOnlinePlayers,
|
||||
boolean includeOfflineLastLocations,
|
||||
boolean includeRespawnLocations,
|
||||
boolean playerLocationsRequireLegacyUser,
|
||||
boolean reconcileOnStartup,
|
||||
boolean allowStartAboveMaxSize,
|
||||
double padding,
|
||||
List<LegacyLocation> locations
|
||||
) {
|
||||
public static LegacyAccessSettings disabled() {
|
||||
return new LegacyAccessSettings(false, false, false, false, false, false, false, false, 0.0, List.of());
|
||||
}
|
||||
}
|
||||
|
||||
public record LegacyLocation(
|
||||
String name,
|
||||
double x,
|
||||
double z
|
||||
) {
|
||||
}
|
||||
|
||||
public record SoftBorderSettings(
|
||||
boolean releaseVanillaBorder,
|
||||
double vanillaBorderSize,
|
||||
boolean ignoreCreative,
|
||||
boolean ignoreSpectator,
|
||||
String bypassPermission,
|
||||
double insideBuffer,
|
||||
double bounceStrength,
|
||||
double verticalBoost,
|
||||
boolean protectMountedEntities,
|
||||
double mountedBounceStrength,
|
||||
double mountedVerticalBoost,
|
||||
double maxOutsideDistanceBeforeTeleport,
|
||||
long cooldownMillis,
|
||||
String message,
|
||||
String actionBar,
|
||||
String sound,
|
||||
float volume,
|
||||
float pitch,
|
||||
boolean particlesEnabled,
|
||||
String particle,
|
||||
int particleCount,
|
||||
double particleOffset,
|
||||
double particleSpeed,
|
||||
long particleIntervalTicks,
|
||||
double particleViewDistance,
|
||||
double particleSpacing
|
||||
) {
|
||||
public static SoftBorderSettings defaults() {
|
||||
return new SoftBorderSettings(
|
||||
true,
|
||||
59_999_968.0,
|
||||
false,
|
||||
true,
|
||||
"dirtsmp.bypass.softborder",
|
||||
1.5,
|
||||
1.85,
|
||||
0.35,
|
||||
true,
|
||||
1.15,
|
||||
0.12,
|
||||
24.0,
|
||||
900L,
|
||||
"{prefix}&#D4AF37&lThe DirtbagMC border &7throws you back.",
|
||||
"&#D4AF37&lBorder reached &8| &7turn back",
|
||||
"ENTITY_SLIME_JUMP",
|
||||
0.65f,
|
||||
0.65f,
|
||||
true,
|
||||
"DUST",
|
||||
3,
|
||||
0.05,
|
||||
0.0,
|
||||
10L,
|
||||
96.0,
|
||||
3.0
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,408 @@
|
||||
package com.dirtsmp.dirtsmp.service;
|
||||
|
||||
import com.dirtsmp.dirtsmp.DirtSMPPlugin;
|
||||
import com.dirtsmp.dirtsmp.config.ConfigManager;
|
||||
import com.dirtsmp.dirtsmp.config.MessageManager;
|
||||
import com.dirtsmp.dirtsmp.model.BorderMode;
|
||||
import com.dirtsmp.dirtsmp.model.ExpansionReason;
|
||||
import com.dirtsmp.dirtsmp.model.ExpansionResult;
|
||||
import com.dirtsmp.dirtsmp.model.GrowthMode;
|
||||
import com.dirtsmp.dirtsmp.model.LegacyAccessReport;
|
||||
import com.dirtsmp.dirtsmp.model.PhaseDefinition;
|
||||
import com.dirtsmp.dirtsmp.model.WorldRule;
|
||||
import com.dirtsmp.dirtsmp.state.PlayerStatsManager;
|
||||
import com.dirtsmp.dirtsmp.state.StateManager;
|
||||
import com.dirtsmp.dirtsmp.state.WorldProgress;
|
||||
import java.util.OptionalDouble;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.WorldBorder;
|
||||
|
||||
public final class BorderManager {
|
||||
private static final double EPSILON = 0.0001;
|
||||
|
||||
private final DirtSMPPlugin plugin;
|
||||
private final ConfigManager configManager;
|
||||
private final MessageManager messageManager;
|
||||
private final StateManager stateManager;
|
||||
private final PlayerStatsManager playerStatsManager;
|
||||
private final CommandHookManager commandHookManager;
|
||||
private final WebhookNotifier webhookNotifier;
|
||||
private final HistoryLogger historyLogger;
|
||||
|
||||
public BorderManager(
|
||||
DirtSMPPlugin plugin,
|
||||
ConfigManager configManager,
|
||||
MessageManager messageManager,
|
||||
StateManager stateManager,
|
||||
PlayerStatsManager playerStatsManager,
|
||||
CommandHookManager commandHookManager,
|
||||
WebhookNotifier webhookNotifier,
|
||||
HistoryLogger historyLogger
|
||||
) {
|
||||
this.plugin = plugin;
|
||||
this.configManager = configManager;
|
||||
this.messageManager = messageManager;
|
||||
this.stateManager = stateManager;
|
||||
this.playerStatsManager = playerStatsManager;
|
||||
this.commandHookManager = commandHookManager;
|
||||
this.webhookNotifier = webhookNotifier;
|
||||
this.historyLogger = historyLogger;
|
||||
}
|
||||
|
||||
public void applyStartupRules() {
|
||||
if (!configManager.applyBordersOnStartup()) {
|
||||
return;
|
||||
}
|
||||
for (WorldRule rule : configManager.worlds().values()) {
|
||||
if (rule.enabled()) {
|
||||
initializeRule(rule);
|
||||
}
|
||||
}
|
||||
stateManager.saveQuietly();
|
||||
}
|
||||
|
||||
public ExpansionResult initializeRule(WorldRule rule) {
|
||||
World world = Bukkit.getWorld(rule.worldName());
|
||||
if (world == null) {
|
||||
plugin.getLogger().warning("Configured world '" + rule.worldName() + "' for key '" + rule.key() + "' is not loaded. Skipping border initialization.");
|
||||
return ExpansionResult.failure("missing world");
|
||||
}
|
||||
|
||||
WorldProgress progress = stateManager.progress(rule.key());
|
||||
WorldBorder border = world.getWorldBorder();
|
||||
if (rule.borderMode() == BorderMode.WORLD_BORDER) {
|
||||
border.setCenter(rule.centerX(), rule.centerZ());
|
||||
} else if (rule.softBorder().releaseVanillaBorder()) {
|
||||
releaseVanillaBorder(world, rule);
|
||||
}
|
||||
|
||||
if (!progress.initialized()) {
|
||||
double size = rule.importCurrentBorder() ? border.getSize() : rule.initialSize();
|
||||
LegacyAccessReport legacyReport = legacyAccessReport(rule);
|
||||
if (legacyReport.requiredSize() > size + EPSILON) {
|
||||
plugin.getLogger().warning("Legacy access increased initial border for '" + rule.key() + "' from " + TimeUtil.formatSize(size) + " to " + TimeUtil.formatSize(legacyReport.requiredSize()) + " blocks to include " + legacyReport.includedLocations() + " known beta location(s). Farthest: " + legacyReport.farthestLocationName() + ".");
|
||||
size = legacyReport.requiredSize();
|
||||
}
|
||||
size = enforceInitialMax(rule, size, legacyReport);
|
||||
if (legacyReport.requiredSize() > size + EPSILON) {
|
||||
plugin.getLogger().warning("Legacy access for '" + rule.key() + "' needs " + TimeUtil.formatSize(legacyReport.requiredSize()) + " blocks, but max-size enforcement capped startup at " + TimeUtil.formatSize(size) + ". Increase max-size or set legacy-access.allow-start-above-max-size: true.");
|
||||
}
|
||||
progress.initialized(true);
|
||||
progress.currentSize(size);
|
||||
progress.currentPhaseIndex(rule.phaseIndexForSize(size));
|
||||
progress.expansionCount(0);
|
||||
progress.lastExpansionAt(System.currentTimeMillis());
|
||||
progress.uniquePlayersAtLastExpansion(playerStatsManager.uniquePlayerCount());
|
||||
progress.clearReminders();
|
||||
applyBorder(world, rule, size, 0L);
|
||||
plugin.getLogger().info("Initialized '" + rule.key() + "' border at " + TimeUtil.formatSize(size) + " blocks.");
|
||||
return ExpansionResult.success(border.getSize(), size, currentPhase(rule, progress));
|
||||
}
|
||||
|
||||
double target = progress.currentSize() > 0.0 ? progress.currentSize() : rule.initialSize();
|
||||
target = enforceMax(rule, target);
|
||||
if (rule.borderMode() == BorderMode.WORLD_BORDER && rule.noShrinkProtection() && border.getSize() > target + EPSILON) {
|
||||
plugin.getLogger().warning("No-shrink protection kept '" + rule.key() + "' at current server border size " + TimeUtil.formatSize(border.getSize()) + " instead of saved size " + TimeUtil.formatSize(target) + ".");
|
||||
target = border.getSize();
|
||||
progress.currentSize(target);
|
||||
progress.currentPhaseIndex(rule.phaseIndexForSize(target));
|
||||
}
|
||||
if (rule.legacyAccess().enabled() && rule.legacyAccess().reconcileOnStartup()) {
|
||||
LegacyAccessReport legacyReport = legacyAccessReport(rule);
|
||||
if (legacyReport.requiredSize() > target + EPSILON) {
|
||||
double adjusted = enforceInitialMax(rule, legacyReport.requiredSize(), legacyReport);
|
||||
if (adjusted > target + EPSILON) {
|
||||
plugin.getLogger().warning("Legacy access reconciled saved border for '" + rule.key() + "' from " + TimeUtil.formatSize(target) + " to " + TimeUtil.formatSize(adjusted) + " blocks to include known beta locations. Farthest: " + legacyReport.farthestLocationName() + ".");
|
||||
target = adjusted;
|
||||
progress.currentSize(target);
|
||||
progress.currentPhaseIndex(rule.phaseIndexForSize(target));
|
||||
}
|
||||
}
|
||||
}
|
||||
applyBorder(world, rule, target, 0L);
|
||||
return ExpansionResult.success(border.getSize(), target, currentPhase(rule, progress));
|
||||
}
|
||||
|
||||
public ExpansionResult expand(WorldRule rule, ExpansionReason reason, String actor, boolean force) {
|
||||
if (!rule.enabled()) {
|
||||
return ExpansionResult.failure("world disabled");
|
||||
}
|
||||
|
||||
World world = Bukkit.getWorld(rule.worldName());
|
||||
if (world == null) {
|
||||
return ExpansionResult.failure(configManager.messages().getString("expansion.missing-world", "missing world"));
|
||||
}
|
||||
|
||||
WorldProgress progress = stateManager.progress(rule.key());
|
||||
if (!progress.initialized()) {
|
||||
initializeRule(rule);
|
||||
}
|
||||
if (reason.automatic() && progress.paused()) {
|
||||
return ExpansionResult.failure(configManager.messages().getString("expansion.paused", "paused"));
|
||||
}
|
||||
if (reason.automatic() && (rule.manualOnly() || rule.triggers().manualOnly())) {
|
||||
return ExpansionResult.failure(configManager.messages().getString("expansion.manual-only", "manual only"));
|
||||
}
|
||||
if (configManager.dryRun()) {
|
||||
return ExpansionResult.failure(configManager.messages().getString("expansion.dry-run", "dry-run"));
|
||||
}
|
||||
|
||||
double oldSize = progress.currentSize() > 0.0 ? progress.currentSize() : world.getWorldBorder().getSize();
|
||||
OptionalDouble next = nextSize(rule, progress);
|
||||
if (next.isEmpty()) {
|
||||
return ExpansionResult.failure(configManager.messages().getString("expansion.max-reached", "max reached"));
|
||||
}
|
||||
double newSize = enforceMax(rule, next.getAsDouble());
|
||||
if (newSize <= oldSize + EPSILON && !force) {
|
||||
return ExpansionResult.failure(configManager.messages().getString("expansion.max-reached", "max reached"));
|
||||
}
|
||||
|
||||
PhaseDefinition phase = phaseAfterExpansion(rule, progress, newSize);
|
||||
commandHookManager.runBefore(rule, oldSize, newSize, reason, actor, phase);
|
||||
applyBorder(world, rule, newSize, rule.transitionSeconds());
|
||||
updateProgressAfterSizeChange(rule, progress, newSize, reason);
|
||||
stateManager.saveQuietly();
|
||||
historyLogger.record(rule, oldSize, newSize, reason, actor, phase);
|
||||
messageManager.announceExpansion(rule, oldSize, newSize, reason, actor, phase);
|
||||
webhookNotifier.notifyExpansion(rule, oldSize, newSize, reason, actor, phase);
|
||||
commandHookManager.runAfter(rule, oldSize, newSize, reason, actor, phase);
|
||||
commandHookManager.runMilestoneRewards(rule, oldSize, newSize, reason, actor, phase, progress.expansionCount());
|
||||
return ExpansionResult.success(oldSize, newSize, phase);
|
||||
}
|
||||
|
||||
public ExpansionResult setSize(WorldRule rule, double size, String actor, boolean force) {
|
||||
if (size <= 0.0) {
|
||||
return ExpansionResult.failure("invalid size");
|
||||
}
|
||||
World world = Bukkit.getWorld(rule.worldName());
|
||||
if (world == null) {
|
||||
return ExpansionResult.failure(configManager.messages().getString("expansion.missing-world", "missing world"));
|
||||
}
|
||||
if (configManager.dryRun()) {
|
||||
return ExpansionResult.failure(configManager.messages().getString("expansion.dry-run", "dry-run"));
|
||||
}
|
||||
|
||||
WorldProgress progress = stateManager.progress(rule.key());
|
||||
if (!progress.initialized()) {
|
||||
initializeRule(rule);
|
||||
}
|
||||
double oldSize = progress.currentSize() > 0.0 ? progress.currentSize() : world.getWorldBorder().getSize();
|
||||
double newSize = force ? size : enforceMax(rule, size);
|
||||
if (rule.noShrinkProtection() && newSize + EPSILON < oldSize && !force) {
|
||||
return ExpansionResult.failure(configManager.messages().getString("expansion.no-shrink", "no shrink"));
|
||||
}
|
||||
|
||||
PhaseDefinition phase = phaseForSize(rule, newSize);
|
||||
applyBorder(world, rule, newSize, rule.transitionSeconds());
|
||||
updateProgressAfterSizeChange(rule, progress, newSize, ExpansionReason.SET_SIZE);
|
||||
stateManager.saveQuietly();
|
||||
historyLogger.record(rule, oldSize, newSize, ExpansionReason.SET_SIZE, actor, phase);
|
||||
return ExpansionResult.success(oldSize, newSize, phase);
|
||||
}
|
||||
|
||||
public ExpansionResult reset(WorldRule rule, String actor, boolean force) {
|
||||
WorldProgress progress = stateManager.progress(rule.key());
|
||||
double oldSize = progress.currentSize();
|
||||
double target = rule.initialSize();
|
||||
if (rule.noShrinkProtection() && target + EPSILON < oldSize && !force) {
|
||||
return ExpansionResult.failure(configManager.messages().getString("expansion.no-shrink", "no shrink"));
|
||||
}
|
||||
|
||||
ExpansionResult result = setSize(rule, target, actor, true);
|
||||
if (!result.success()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
progress.initialized(true);
|
||||
progress.currentSize(target);
|
||||
progress.currentPhaseIndex(rule.phaseIndexForSize(target));
|
||||
progress.expansionCount(0);
|
||||
progress.paused(false);
|
||||
progress.lastExpansionAt(System.currentTimeMillis());
|
||||
progress.uniquePlayersAtLastExpansion(playerStatsManager.uniquePlayerCount());
|
||||
progress.clearReminders();
|
||||
stateManager.saveQuietly();
|
||||
historyLogger.record(rule, oldSize, target, ExpansionReason.RESET, actor, currentPhase(rule, progress));
|
||||
return ExpansionResult.success(oldSize, target, currentPhase(rule, progress));
|
||||
}
|
||||
|
||||
public void pause(WorldRule rule) {
|
||||
WorldProgress progress = stateManager.progress(rule.key());
|
||||
progress.paused(true);
|
||||
stateManager.saveQuietly();
|
||||
}
|
||||
|
||||
public void resume(WorldRule rule) {
|
||||
WorldProgress progress = stateManager.progress(rule.key());
|
||||
progress.paused(false);
|
||||
stateManager.saveQuietly();
|
||||
}
|
||||
|
||||
public OptionalDouble nextSize(WorldRule rule, WorldProgress progress) {
|
||||
double currentSize = progress.currentSize() > 0.0 ? progress.currentSize() : rule.initialSize();
|
||||
if (rule.growthMode() == GrowthMode.PHASE) {
|
||||
return rule.nextPhase(progress.currentPhaseIndex()).map(PhaseDefinition::size).map(OptionalDouble::of).orElseGet(OptionalDouble::empty);
|
||||
}
|
||||
return OptionalDouble.of(currentSize + rule.growthAmount());
|
||||
}
|
||||
|
||||
public double displaySize(WorldRule rule, WorldProgress progress) {
|
||||
return progress.currentSize() > 0.0 ? progress.currentSize() : rule.initialSize();
|
||||
}
|
||||
|
||||
public String nextSizeDisplay(WorldRule rule, WorldProgress progress) {
|
||||
OptionalDouble next = nextSize(rule, progress);
|
||||
if (next.isEmpty()) {
|
||||
return "max";
|
||||
}
|
||||
return TimeUtil.formatSize(enforceMax(rule, next.getAsDouble()));
|
||||
}
|
||||
|
||||
public PhaseDefinition currentPhase(WorldRule rule, WorldProgress progress) {
|
||||
return rule.phase(progress.currentPhaseIndex()).orElse(null);
|
||||
}
|
||||
|
||||
public PhaseDefinition phaseForSize(WorldRule rule, double size) {
|
||||
return rule.phase(rule.phaseIndexForSize(size)).orElse(null);
|
||||
}
|
||||
|
||||
public LegacyAccessReport legacyAccessReport(WorldRule rule) {
|
||||
World world = Bukkit.getWorld(rule.worldName());
|
||||
if (world == null || !rule.legacyAccess().enabled()) {
|
||||
return new LegacyAccessReport(0.0, 0, "none");
|
||||
}
|
||||
|
||||
LegacyAccumulator accumulator = new LegacyAccumulator();
|
||||
WorldRule.LegacyAccessSettings access = rule.legacyAccess();
|
||||
if (access.includeCurrentBorder()) {
|
||||
accumulator.include(world.getWorldBorder().getSize(), "current world border");
|
||||
}
|
||||
if (access.includeOnlinePlayers()) {
|
||||
for (org.bukkit.entity.Player player : Bukkit.getOnlinePlayers()) {
|
||||
if (!includePlayerLocation(rule, player)) {
|
||||
continue;
|
||||
}
|
||||
includeLocation(rule, accumulator, player.getName() + " online location", player.getLocation());
|
||||
}
|
||||
}
|
||||
if (access.includeOfflineLastLocations() || access.includeRespawnLocations()) {
|
||||
for (OfflinePlayer player : Bukkit.getOfflinePlayers()) {
|
||||
if (!includePlayerLocation(rule, player)) {
|
||||
continue;
|
||||
}
|
||||
String name = player.getName() == null ? player.getUniqueId().toString() : player.getName();
|
||||
if (access.includeOfflineLastLocations()) {
|
||||
includeLocation(rule, accumulator, name + " last location", player.getLocation());
|
||||
}
|
||||
if (access.includeRespawnLocations()) {
|
||||
includeLocation(rule, accumulator, name + " respawn location", player.getRespawnLocation());
|
||||
}
|
||||
}
|
||||
}
|
||||
for (WorldRule.LegacyLocation location : access.locations()) {
|
||||
includeCoordinate(rule, accumulator, location.name(), location.x(), location.z());
|
||||
}
|
||||
return new LegacyAccessReport(accumulator.requiredSize, accumulator.includedLocations, accumulator.farthestLocationName);
|
||||
}
|
||||
|
||||
private PhaseDefinition phaseAfterExpansion(WorldRule rule, WorldProgress progress, double newSize) {
|
||||
if (rule.growthMode() == GrowthMode.PHASE) {
|
||||
return rule.nextPhase(progress.currentPhaseIndex()).orElse(phaseForSize(rule, newSize));
|
||||
}
|
||||
return phaseForSize(rule, newSize);
|
||||
}
|
||||
|
||||
private void updateProgressAfterSizeChange(WorldRule rule, WorldProgress progress, double newSize, ExpansionReason reason) {
|
||||
progress.initialized(true);
|
||||
progress.currentSize(newSize);
|
||||
progress.currentPhaseIndex(rule.phaseIndexForSize(newSize));
|
||||
if (reason != ExpansionReason.SET_SIZE) {
|
||||
progress.expansionCount(progress.expansionCount() + 1);
|
||||
}
|
||||
progress.lastExpansionAt(System.currentTimeMillis());
|
||||
progress.uniquePlayersAtLastExpansion(playerStatsManager.uniquePlayerCount());
|
||||
progress.clearReminders();
|
||||
}
|
||||
|
||||
private double enforceMax(WorldRule rule, double size) {
|
||||
if (!rule.enforceMaxSize()) {
|
||||
return size;
|
||||
}
|
||||
return Math.min(size, rule.maxSize());
|
||||
}
|
||||
|
||||
private double enforceInitialMax(WorldRule rule, double size, LegacyAccessReport legacyReport) {
|
||||
if (!rule.enforceMaxSize()) {
|
||||
return size;
|
||||
}
|
||||
if (rule.legacyAccess().enabled()
|
||||
&& rule.legacyAccess().allowStartAboveMaxSize()
|
||||
&& legacyReport.requiredSize() > rule.maxSize()
|
||||
&& size >= legacyReport.requiredSize() - EPSILON) {
|
||||
return size;
|
||||
}
|
||||
return Math.min(size, rule.maxSize());
|
||||
}
|
||||
|
||||
private void includeLocation(WorldRule rule, LegacyAccumulator accumulator, String name, Location location) {
|
||||
if (location == null || location.getWorld() == null || !location.getWorld().getName().equals(rule.worldName())) {
|
||||
return;
|
||||
}
|
||||
includeCoordinate(rule, accumulator, name, location.getX(), location.getZ());
|
||||
}
|
||||
|
||||
private boolean includePlayerLocation(WorldRule rule, OfflinePlayer player) {
|
||||
return !rule.legacyAccess().playerLocationsRequireLegacyUser() || stateManager.isLegacyUser(player.getUniqueId());
|
||||
}
|
||||
|
||||
private void includeCoordinate(WorldRule rule, LegacyAccumulator accumulator, String name, double x, double z) {
|
||||
double halfSize = Math.max(Math.abs(x - rule.centerX()), Math.abs(z - rule.centerZ())) + rule.legacyAccess().padding();
|
||||
accumulator.include(halfSize * 2.0, name);
|
||||
}
|
||||
|
||||
private void applyBorder(World world, WorldRule rule, double size, long transitionSeconds) {
|
||||
if (configManager.dryRun()) {
|
||||
plugin.getLogger().info("[dry-run] Would set '" + rule.key() + "' border to " + TimeUtil.formatSize(size) + " blocks.");
|
||||
return;
|
||||
}
|
||||
if (rule.borderMode() == BorderMode.SOFT_BORDER) {
|
||||
if (rule.softBorder().releaseVanillaBorder()) {
|
||||
releaseVanillaBorder(world, rule);
|
||||
}
|
||||
plugin.getLogger().info("Set soft border state for '" + rule.key() + "' to " + TimeUtil.formatSize(size) + " blocks. Vanilla world border was not used for enforcement.");
|
||||
return;
|
||||
}
|
||||
WorldBorder border = world.getWorldBorder();
|
||||
border.setCenter(rule.centerX(), rule.centerZ());
|
||||
if (transitionSeconds > 0L) {
|
||||
border.setSize(size, transitionSeconds);
|
||||
} else {
|
||||
border.setSize(size);
|
||||
}
|
||||
}
|
||||
|
||||
private void releaseVanillaBorder(World world, WorldRule rule) {
|
||||
WorldBorder border = world.getWorldBorder();
|
||||
border.setCenter(rule.centerX(), rule.centerZ());
|
||||
if (border.getSize() < rule.softBorder().vanillaBorderSize() - EPSILON) {
|
||||
border.setSize(rule.softBorder().vanillaBorderSize());
|
||||
}
|
||||
}
|
||||
|
||||
private static final class LegacyAccumulator {
|
||||
private double requiredSize;
|
||||
private int includedLocations;
|
||||
private String farthestLocationName = "none";
|
||||
|
||||
private void include(double requiredSize, String name) {
|
||||
includedLocations++;
|
||||
if (requiredSize > this.requiredSize) {
|
||||
this.requiredSize = requiredSize;
|
||||
this.farthestLocationName = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.dirtsmp.dirtsmp.service;
|
||||
|
||||
import com.dirtsmp.dirtsmp.config.ConfigManager;
|
||||
import com.dirtsmp.dirtsmp.config.MessageManager;
|
||||
import com.dirtsmp.dirtsmp.model.ExpansionReason;
|
||||
import com.dirtsmp.dirtsmp.model.PhaseDefinition;
|
||||
import com.dirtsmp.dirtsmp.model.WorldRule;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Locale;
|
||||
import org.bukkit.Bukkit;
|
||||
|
||||
public final class CommandHookManager {
|
||||
private final ConfigManager configManager;
|
||||
private final MessageManager messageManager;
|
||||
|
||||
public CommandHookManager(ConfigManager configManager, MessageManager messageManager) {
|
||||
this.configManager = configManager;
|
||||
this.messageManager = messageManager;
|
||||
}
|
||||
|
||||
public void runBefore(WorldRule rule, double oldSize, double newSize, ExpansionReason reason, String actor, PhaseDefinition phase) {
|
||||
runCommands(commands(configManager.globalBeforeCommands(), rule.beforeCommands()), rule, oldSize, newSize, reason, actor, phase, Map.of());
|
||||
}
|
||||
|
||||
public void runAfter(WorldRule rule, double oldSize, double newSize, ExpansionReason reason, String actor, PhaseDefinition phase) {
|
||||
runCommands(commands(configManager.globalAfterCommands(), rule.afterCommands()), rule, oldSize, newSize, reason, actor, phase, Map.of());
|
||||
}
|
||||
|
||||
public void runMilestoneRewards(WorldRule rule, double oldSize, double newSize, ExpansionReason reason, String actor, PhaseDefinition phase, int expansionCount) {
|
||||
List<String> commands = new ArrayList<>();
|
||||
commands.addAll(rewardCommands(configManager.globalMilestoneRewards(), expansionCount, phase));
|
||||
commands.addAll(rewardCommands(rule.milestoneRewards(), expansionCount, phase));
|
||||
|
||||
runCommands(commands, rule, oldSize, newSize, reason, actor, phase, Map.of(
|
||||
"expansion_count", String.valueOf(expansionCount)
|
||||
));
|
||||
}
|
||||
|
||||
private List<String> commands(List<String> global, List<String> worldSpecific) {
|
||||
List<String> commands = new ArrayList<>();
|
||||
commands.addAll(global);
|
||||
commands.addAll(worldSpecific);
|
||||
return commands;
|
||||
}
|
||||
|
||||
private List<String> rewardCommands(WorldRule.MilestoneRewardSettings settings, int expansionCount, PhaseDefinition phase) {
|
||||
if (!settings.enabled()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
List<String> commands = new ArrayList<>();
|
||||
commands.addAll(settings.everyExpansionCommands());
|
||||
commands.addAll(settings.expansionCountCommands().getOrDefault(expansionCount, List.of()));
|
||||
if (phase != null) {
|
||||
commands.addAll(settings.phaseIndexCommands().getOrDefault(phase.index(), List.of()));
|
||||
commands.addAll(settings.phaseNameCommands().getOrDefault(phase.name().toLowerCase(Locale.ROOT), List.of()));
|
||||
}
|
||||
return commands;
|
||||
}
|
||||
|
||||
private void runCommands(List<String> commands, WorldRule rule, double oldSize, double newSize, ExpansionReason reason, String actor, PhaseDefinition phase, Map<String, String> extraPlaceholders) {
|
||||
if (!configManager.hooksEnabled() || commands.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, String> placeholders = messageManager.withBasePlaceholders(rule, oldSize, newSize, reason, actor, phase, extraPlaceholders);
|
||||
for (String command : commands) {
|
||||
if (command == null || command.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
String rendered = messageManager.applyPlaceholders(command, placeholders);
|
||||
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), rendered.startsWith("/") ? rendered.substring(1) : rendered);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.dirtsmp.dirtsmp.service;
|
||||
|
||||
import com.dirtsmp.dirtsmp.DirtSMPPlugin;
|
||||
import com.dirtsmp.dirtsmp.config.ConfigManager;
|
||||
import com.dirtsmp.dirtsmp.model.ExpansionReason;
|
||||
import com.dirtsmp.dirtsmp.model.PhaseDefinition;
|
||||
import com.dirtsmp.dirtsmp.model.WorldRule;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.time.Instant;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public final class HistoryLogger {
|
||||
private final DirtSMPPlugin plugin;
|
||||
private final ConfigManager configManager;
|
||||
|
||||
public HistoryLogger(DirtSMPPlugin plugin, ConfigManager configManager) {
|
||||
this.plugin = plugin;
|
||||
this.configManager = configManager;
|
||||
}
|
||||
|
||||
public void record(WorldRule rule, double oldSize, double newSize, ExpansionReason reason, String actor, PhaseDefinition phase) {
|
||||
if (!configManager.historyEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
File file = new File(plugin.getDataFolder(), configManager.historyFileName());
|
||||
String line = String.join(" | ",
|
||||
Instant.now().toString(),
|
||||
"world=" + rule.worldName(),
|
||||
"key=" + rule.key(),
|
||||
"old=" + TimeUtil.formatSize(oldSize),
|
||||
"new=" + TimeUtil.formatSize(newSize),
|
||||
"reason=" + reason.name(),
|
||||
"actor=" + (actor == null ? "system" : actor),
|
||||
"phase=" + (phase == null ? "none" : phase.name())
|
||||
) + System.lineSeparator();
|
||||
|
||||
try {
|
||||
Files.writeString(file.toPath(), line, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
|
||||
} catch (IOException ex) {
|
||||
plugin.getLogger().log(Level.WARNING, "Could not append to expansion history.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
package com.dirtsmp.dirtsmp.service;
|
||||
|
||||
import com.dirtsmp.dirtsmp.DirtSMPPlugin;
|
||||
import com.dirtsmp.dirtsmp.config.ConfigManager;
|
||||
import com.dirtsmp.dirtsmp.config.MessageManager;
|
||||
import com.dirtsmp.dirtsmp.model.BorderMode;
|
||||
import com.dirtsmp.dirtsmp.model.WorldRule;
|
||||
import com.dirtsmp.dirtsmp.state.StateManager;
|
||||
import com.dirtsmp.dirtsmp.state.WorldProgress;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Color;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Particle;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.SoundCategory;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.entity.EntityTeleportEvent;
|
||||
import org.bukkit.event.player.PlayerMoveEvent;
|
||||
import org.bukkit.event.player.PlayerTeleportEvent;
|
||||
import org.bukkit.event.vehicle.VehicleMoveEvent;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
public final class SoftBorderManager implements Listener {
|
||||
private static final double EPSILON = 0.0001;
|
||||
private static final long TASK_PERIOD_TICKS = 5L;
|
||||
|
||||
private final DirtSMPPlugin plugin;
|
||||
private final ConfigManager configManager;
|
||||
private final MessageManager messageManager;
|
||||
private final StateManager stateManager;
|
||||
private final Map<UUID, Long> feedbackCooldowns = new HashMap<>();
|
||||
private final Set<UUID> correctingEntities = new HashSet<>();
|
||||
private BukkitTask task;
|
||||
private long tickCounter;
|
||||
|
||||
public SoftBorderManager(DirtSMPPlugin plugin, ConfigManager configManager, MessageManager messageManager, StateManager stateManager) {
|
||||
this.plugin = plugin;
|
||||
this.configManager = configManager;
|
||||
this.messageManager = messageManager;
|
||||
this.stateManager = stateManager;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
stop();
|
||||
task = Bukkit.getScheduler().runTaskTimer(plugin, this::tick, TASK_PERIOD_TICKS, TASK_PERIOD_TICKS);
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (task != null) {
|
||||
task.cancel();
|
||||
task = null;
|
||||
}
|
||||
feedbackCooldowns.clear();
|
||||
correctingEntities.clear();
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onMove(PlayerMoveEvent event) {
|
||||
Location to = event.getTo();
|
||||
if (!movedHorizontally(event.getFrom(), to)) {
|
||||
return;
|
||||
}
|
||||
WorldRule rule = softRule(to.getWorld());
|
||||
if (rule == null || exempt(event.getPlayer(), rule)) {
|
||||
return;
|
||||
}
|
||||
Entity target = protectedEntity(event.getPlayer(), rule);
|
||||
Location targetLocation = target.getLocation();
|
||||
if (!outside(rule, targetLocation)) {
|
||||
return;
|
||||
}
|
||||
|
||||
double outsideDistance = outsideDistance(rule, targetLocation);
|
||||
if (outsideDistance >= rule.softBorder().maxOutsideDistanceBeforeTeleport()) {
|
||||
teleportInside(target, event.getPlayer(), rule, targetLocation);
|
||||
return;
|
||||
}
|
||||
bounce(target, event.getPlayer(), rule, targetLocation);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onTeleport(PlayerTeleportEvent event) {
|
||||
if (correctingEntities.remove(event.getPlayer().getUniqueId())) {
|
||||
return;
|
||||
}
|
||||
|
||||
Location to = event.getTo();
|
||||
if (to == null) {
|
||||
return;
|
||||
}
|
||||
WorldRule rule = softRule(to.getWorld());
|
||||
if (rule == null || exempt(event.getPlayer(), rule) || !outside(rule, to)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Location corrected = closestInside(rule, to);
|
||||
event.setTo(corrected);
|
||||
feedback(event.getPlayer(), rule, true);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onEntityTeleport(EntityTeleportEvent event) {
|
||||
Entity entity = event.getEntity();
|
||||
if (entity instanceof Player || correctingEntities.remove(entity.getUniqueId())) {
|
||||
return;
|
||||
}
|
||||
|
||||
Location to = event.getTo();
|
||||
WorldRule rule = to == null ? null : softRule(to.getWorld());
|
||||
if (rule == null || !rule.softBorder().protectMountedEntities() || !outside(rule, to)) {
|
||||
return;
|
||||
}
|
||||
Player rider = firstNonExemptPassenger(entity, rule);
|
||||
if (rider == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.setTo(closestInside(rule, to));
|
||||
feedback(rider, rule, true);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onVehicleMove(VehicleMoveEvent event) {
|
||||
if (!movedHorizontally(event.getFrom(), event.getTo())) {
|
||||
return;
|
||||
}
|
||||
|
||||
WorldRule rule = softRule(event.getTo().getWorld());
|
||||
if (rule == null || !rule.softBorder().protectMountedEntities() || !outside(rule, event.getTo())) {
|
||||
return;
|
||||
}
|
||||
Player rider = firstNonExemptPassenger(event.getVehicle(), rule);
|
||||
if (rider == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
double outsideDistance = outsideDistance(rule, event.getTo());
|
||||
if (outsideDistance >= rule.softBorder().maxOutsideDistanceBeforeTeleport()) {
|
||||
teleportInside(event.getVehicle(), rider, rule, event.getTo());
|
||||
return;
|
||||
}
|
||||
bounce(event.getVehicle(), rider, rule, event.getTo());
|
||||
}
|
||||
|
||||
private void tick() {
|
||||
tickCounter += TASK_PERIOD_TICKS;
|
||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||
WorldRule rule = softRule(player.getWorld());
|
||||
if (rule == null || exempt(player, rule)) {
|
||||
continue;
|
||||
}
|
||||
Entity target = protectedEntity(player, rule);
|
||||
Location targetLocation = target.getLocation();
|
||||
if (outside(rule, targetLocation)) {
|
||||
teleportInside(target, player, rule, targetLocation);
|
||||
continue;
|
||||
}
|
||||
if (rule.softBorder().particlesEnabled() && tickCounter % rule.softBorder().particleIntervalTicks() == 0L) {
|
||||
renderParticles(player, rule);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private WorldRule softRule(World world) {
|
||||
if (world == null) {
|
||||
return null;
|
||||
}
|
||||
for (WorldRule rule : configManager.worlds().values()) {
|
||||
if (rule.enabled() && rule.borderMode() == BorderMode.SOFT_BORDER && rule.worldName().equals(world.getName())) {
|
||||
return rule;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean exempt(Player player, WorldRule rule) {
|
||||
WorldRule.SoftBorderSettings settings = rule.softBorder();
|
||||
if (!settings.bypassPermission().isBlank() && player.hasPermission(settings.bypassPermission())) {
|
||||
return true;
|
||||
}
|
||||
if (settings.ignoreCreative() && player.getGameMode() == GameMode.CREATIVE) {
|
||||
return true;
|
||||
}
|
||||
return settings.ignoreSpectator() && player.getGameMode() == GameMode.SPECTATOR;
|
||||
}
|
||||
|
||||
private boolean movedHorizontally(Location from, Location to) {
|
||||
return to != null
|
||||
&& from.getWorld() == to.getWorld()
|
||||
&& (Math.abs(from.getX() - to.getX()) > EPSILON || Math.abs(from.getZ() - to.getZ()) > EPSILON);
|
||||
}
|
||||
|
||||
private Entity protectedEntity(Player player, WorldRule rule) {
|
||||
if (!rule.softBorder().protectMountedEntities() || !player.isInsideVehicle()) {
|
||||
return player;
|
||||
}
|
||||
Entity vehicle = player.getVehicle();
|
||||
if (vehicle == null || vehicle.getWorld() != player.getWorld()) {
|
||||
return player;
|
||||
}
|
||||
return vehicle;
|
||||
}
|
||||
|
||||
private Player firstNonExemptPassenger(Entity entity, WorldRule rule) {
|
||||
for (Entity passenger : entity.getPassengers()) {
|
||||
if (passenger instanceof Player player && !exempt(player, rule)) {
|
||||
return player;
|
||||
}
|
||||
Player nested = firstNonExemptPassenger(passenger, rule);
|
||||
if (nested != null) {
|
||||
return nested;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean outside(WorldRule rule, Location location) {
|
||||
Bounds bounds = bounds(rule);
|
||||
return location.getX() < bounds.minX
|
||||
|| location.getX() > bounds.maxX
|
||||
|| location.getZ() < bounds.minZ
|
||||
|| location.getZ() > bounds.maxZ;
|
||||
}
|
||||
|
||||
private double outsideDistance(WorldRule rule, Location location) {
|
||||
Bounds bounds = bounds(rule);
|
||||
double outsideX = Math.max(bounds.minX - location.getX(), location.getX() - bounds.maxX);
|
||||
double outsideZ = Math.max(bounds.minZ - location.getZ(), location.getZ() - bounds.maxZ);
|
||||
return Math.max(Math.max(0.0, outsideX), Math.max(0.0, outsideZ));
|
||||
}
|
||||
|
||||
private void bounce(Entity target, Player player, WorldRule rule, Location location) {
|
||||
Bounds bounds = bounds(rule);
|
||||
double pushX = 0.0;
|
||||
double pushZ = 0.0;
|
||||
if (location.getX() < bounds.minX) {
|
||||
pushX = 1.0;
|
||||
} else if (location.getX() > bounds.maxX) {
|
||||
pushX = -1.0;
|
||||
}
|
||||
if (location.getZ() < bounds.minZ) {
|
||||
pushZ = 1.0;
|
||||
} else if (location.getZ() > bounds.maxZ) {
|
||||
pushZ = -1.0;
|
||||
}
|
||||
|
||||
Vector velocity = new Vector(pushX, 0.0, pushZ);
|
||||
if (velocity.lengthSquared() <= EPSILON) {
|
||||
velocity = location.toVector().subtract(closestInside(rule, location).toVector()).multiply(-1.0);
|
||||
}
|
||||
velocity.setY(0.0);
|
||||
if (velocity.lengthSquared() <= EPSILON) {
|
||||
velocity = new Vector(rule.centerX() - location.getX(), 0.0, rule.centerZ() - location.getZ());
|
||||
}
|
||||
double strength = target instanceof Player ? rule.softBorder().bounceStrength() : rule.softBorder().mountedBounceStrength();
|
||||
double vertical = target instanceof Player ? rule.softBorder().verticalBoost() : rule.softBorder().mountedVerticalBoost();
|
||||
velocity.normalize().multiply(strength).setY(vertical);
|
||||
target.setVelocity(velocity);
|
||||
feedback(player, rule, false);
|
||||
}
|
||||
|
||||
private void teleportInside(Entity target, Player player, WorldRule rule, Location from) {
|
||||
Location corrected = closestInside(rule, from);
|
||||
Set<Entity> passengers = new HashSet<>(target.getPassengers());
|
||||
correctingEntities.add(target.getUniqueId());
|
||||
target.teleport(corrected);
|
||||
restorePassengers(target, passengers);
|
||||
feedback(player, rule, true);
|
||||
}
|
||||
|
||||
private void restorePassengers(Entity target, Set<Entity> passengers) {
|
||||
if (passengers.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
if (!target.isValid()) {
|
||||
return;
|
||||
}
|
||||
for (Entity passenger : passengers) {
|
||||
if (!passenger.isValid() || target.getPassengers().contains(passenger)) {
|
||||
continue;
|
||||
}
|
||||
correctingEntities.add(passenger.getUniqueId());
|
||||
passenger.teleport(target.getLocation());
|
||||
target.addPassenger(passenger);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Location closestInside(WorldRule rule, Location location) {
|
||||
Bounds bounds = bounds(rule);
|
||||
double buffer = rule.softBorder().insideBuffer();
|
||||
double minX = bounds.minX + buffer;
|
||||
double maxX = bounds.maxX - buffer;
|
||||
double minZ = bounds.minZ + buffer;
|
||||
double maxZ = bounds.maxZ - buffer;
|
||||
double x = Math.max(minX, Math.min(maxX, location.getX()));
|
||||
double z = Math.max(minZ, Math.min(maxZ, location.getZ()));
|
||||
|
||||
Location corrected = location.clone();
|
||||
corrected.setX(x);
|
||||
corrected.setZ(z);
|
||||
return corrected;
|
||||
}
|
||||
|
||||
private Bounds bounds(WorldRule rule) {
|
||||
WorldProgress progress = stateManager.progress(rule.key());
|
||||
double size = progress.currentSize() > 0.0 ? progress.currentSize() : rule.initialSize();
|
||||
double half = size / 2.0;
|
||||
return new Bounds(rule.centerX() - half, rule.centerX() + half, rule.centerZ() - half, rule.centerZ() + half);
|
||||
}
|
||||
|
||||
private void feedback(Player player, WorldRule rule, boolean forcedTeleport) {
|
||||
long now = System.currentTimeMillis();
|
||||
long last = feedbackCooldowns.getOrDefault(player.getUniqueId(), 0L);
|
||||
if (now - last < rule.softBorder().cooldownMillis()) {
|
||||
return;
|
||||
}
|
||||
feedbackCooldowns.put(player.getUniqueId(), now);
|
||||
|
||||
Map<String, String> placeholders = Map.of(
|
||||
"world", rule.worldName(),
|
||||
"key", rule.key(),
|
||||
"mode", forcedTeleport ? "corrected" : "bounced"
|
||||
);
|
||||
messageManager.sendRaw(player, rule.softBorder().message(), placeholders);
|
||||
if (!rule.softBorder().actionBar().isBlank()) {
|
||||
player.sendActionBar(messageManager.component(rule.softBorder().actionBar(), placeholders));
|
||||
}
|
||||
playSound(player, rule);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"deprecation", "removal"})
|
||||
private void playSound(Player player, WorldRule rule) {
|
||||
String soundName = rule.softBorder().sound();
|
||||
if (soundName == null || soundName.isBlank()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Sound sound = Sound.valueOf(soundName.trim().toUpperCase(Locale.ROOT));
|
||||
player.playSound(player.getLocation(), sound, SoundCategory.MASTER, rule.softBorder().volume(), rule.softBorder().pitch());
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
player.playSound(player.getLocation(), soundName.toLowerCase(Locale.ROOT), SoundCategory.MASTER, rule.softBorder().volume(), rule.softBorder().pitch());
|
||||
}
|
||||
}
|
||||
|
||||
private void renderParticles(Player player, WorldRule rule) {
|
||||
Bounds bounds = bounds(rule);
|
||||
Location playerLocation = player.getLocation();
|
||||
double view = rule.softBorder().particleViewDistance();
|
||||
double spacing = rule.softBorder().particleSpacing();
|
||||
double y = playerLocation.getY() + 1.1;
|
||||
|
||||
if (Math.abs(playerLocation.getX() - bounds.minX) <= view) {
|
||||
renderLine(player, rule, bounds.minX, clamp(playerLocation.getZ() - view, bounds.minZ, bounds.maxZ), bounds.minX, clamp(playerLocation.getZ() + view, bounds.minZ, bounds.maxZ), y, spacing);
|
||||
}
|
||||
if (Math.abs(playerLocation.getX() - bounds.maxX) <= view) {
|
||||
renderLine(player, rule, bounds.maxX, clamp(playerLocation.getZ() - view, bounds.minZ, bounds.maxZ), bounds.maxX, clamp(playerLocation.getZ() + view, bounds.minZ, bounds.maxZ), y, spacing);
|
||||
}
|
||||
if (Math.abs(playerLocation.getZ() - bounds.minZ) <= view) {
|
||||
renderLine(player, rule, clamp(playerLocation.getX() - view, bounds.minX, bounds.maxX), bounds.minZ, clamp(playerLocation.getX() + view, bounds.minX, bounds.maxX), bounds.minZ, y, spacing);
|
||||
}
|
||||
if (Math.abs(playerLocation.getZ() - bounds.maxZ) <= view) {
|
||||
renderLine(player, rule, clamp(playerLocation.getX() - view, bounds.minX, bounds.maxX), bounds.maxZ, clamp(playerLocation.getX() + view, bounds.minX, bounds.maxX), bounds.maxZ, y, spacing);
|
||||
}
|
||||
}
|
||||
|
||||
private void renderLine(Player player, WorldRule rule, double startX, double startZ, double endX, double endZ, double y, double spacing) {
|
||||
double distance = Math.hypot(endX - startX, endZ - startZ);
|
||||
int steps = Math.max(1, (int) Math.floor(distance / spacing));
|
||||
for (int step = 0; step <= steps; step++) {
|
||||
double t = (double) step / (double) steps;
|
||||
double x = startX + ((endX - startX) * t);
|
||||
double z = startZ + ((endZ - startZ) * t);
|
||||
spawnParticle(player, rule, x, y, z);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
private void spawnParticle(Player player, WorldRule rule, double x, double y, double z) {
|
||||
WorldRule.SoftBorderSettings settings = rule.softBorder();
|
||||
try {
|
||||
Particle particle = Particle.valueOf(settings.particle().trim().toUpperCase(Locale.ROOT));
|
||||
if (particle == Particle.DUST) {
|
||||
player.spawnParticle(
|
||||
Particle.DUST,
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
settings.particleCount(),
|
||||
settings.particleOffset(),
|
||||
settings.particleOffset(),
|
||||
settings.particleOffset(),
|
||||
settings.particleSpeed(),
|
||||
new Particle.DustOptions(Color.fromRGB(212, 175, 55), 1.25f)
|
||||
);
|
||||
return;
|
||||
}
|
||||
player.spawnParticle(
|
||||
particle,
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
settings.particleCount(),
|
||||
settings.particleOffset(),
|
||||
settings.particleOffset(),
|
||||
settings.particleOffset(),
|
||||
settings.particleSpeed()
|
||||
);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
player.spawnParticle(Particle.END_ROD, x, y, z, 1, 0.02, 0.02, 0.02, 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
private double clamp(double value, double min, double max) {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
private record Bounds(double minX, double maxX, double minZ, double maxZ) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.dirtsmp.dirtsmp.service;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public final class TimeUtil {
|
||||
private static final Pattern DURATION_PART = Pattern.compile("(\\d+(?:\\.\\d+)?)(ms|s|m|h|d|w)?", Pattern.CASE_INSENSITIVE);
|
||||
private static final DecimalFormat SIZE_FORMAT = new DecimalFormat("#,##0.##");
|
||||
|
||||
private TimeUtil() {
|
||||
}
|
||||
|
||||
public static long parseDurationMillis(String input, long fallbackMillis) {
|
||||
if (input == null || input.isBlank()) {
|
||||
return fallbackMillis;
|
||||
}
|
||||
|
||||
String compact = input.trim().toLowerCase(Locale.ROOT).replace(" ", "");
|
||||
Matcher matcher = DURATION_PART.matcher(compact);
|
||||
long total = 0L;
|
||||
int matched = 0;
|
||||
|
||||
while (matcher.find()) {
|
||||
if (matcher.start() != matched) {
|
||||
return fallbackMillis;
|
||||
}
|
||||
double amount = Double.parseDouble(matcher.group(1));
|
||||
String unit = matcher.group(2);
|
||||
total += switch (unit == null ? "s" : unit) {
|
||||
case "ms" -> Math.round(amount);
|
||||
case "s" -> Math.round(amount * 1000.0);
|
||||
case "m" -> Math.round(amount * 60_000.0);
|
||||
case "h" -> Math.round(amount * 3_600_000.0);
|
||||
case "d" -> Math.round(amount * 86_400_000.0);
|
||||
case "w" -> Math.round(amount * 604_800_000.0);
|
||||
default -> fallbackMillis;
|
||||
};
|
||||
matched = matcher.end();
|
||||
}
|
||||
|
||||
return matched == compact.length() && total > 0L ? total : fallbackMillis;
|
||||
}
|
||||
|
||||
public static String formatDuration(long millis) {
|
||||
if (millis <= 0L) {
|
||||
return "now";
|
||||
}
|
||||
|
||||
long seconds = millis / 1000L;
|
||||
long days = seconds / 86_400L;
|
||||
seconds %= 86_400L;
|
||||
long hours = seconds / 3_600L;
|
||||
seconds %= 3_600L;
|
||||
long minutes = seconds / 60L;
|
||||
seconds %= 60L;
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
appendPart(builder, days, "d");
|
||||
appendPart(builder, hours, "h");
|
||||
appendPart(builder, minutes, "m");
|
||||
if (builder.isEmpty()) {
|
||||
appendPart(builder, seconds, "s");
|
||||
}
|
||||
return builder.toString().trim();
|
||||
}
|
||||
|
||||
private static void appendPart(StringBuilder builder, long value, String suffix) {
|
||||
if (value <= 0L) {
|
||||
return;
|
||||
}
|
||||
if (!builder.isEmpty()) {
|
||||
builder.append(' ');
|
||||
}
|
||||
builder.append(value).append(suffix);
|
||||
}
|
||||
|
||||
public static String formatSize(double size) {
|
||||
return SIZE_FORMAT.format(size);
|
||||
}
|
||||
|
||||
public static String formatInstant(long epochMillis) {
|
||||
if (epochMillis <= 0L) {
|
||||
return "unknown";
|
||||
}
|
||||
return DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(Instant.ofEpochMilli(epochMillis).atZone(ZoneId.systemDefault()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package com.dirtsmp.dirtsmp.service;
|
||||
|
||||
import com.dirtsmp.dirtsmp.model.ExpansionReason;
|
||||
import com.dirtsmp.dirtsmp.model.TriggerDecision;
|
||||
import com.dirtsmp.dirtsmp.model.TriggerMode;
|
||||
import com.dirtsmp.dirtsmp.model.TriggerSettings;
|
||||
import com.dirtsmp.dirtsmp.model.WorldRule;
|
||||
import com.dirtsmp.dirtsmp.state.PlayerStatsManager;
|
||||
import com.dirtsmp.dirtsmp.state.WorldProgress;
|
||||
|
||||
public final class TriggerEvaluator {
|
||||
private final PlayerStatsManager playerStatsManager;
|
||||
|
||||
public TriggerEvaluator(PlayerStatsManager playerStatsManager) {
|
||||
this.playerStatsManager = playerStatsManager;
|
||||
}
|
||||
|
||||
public TriggerDecision evaluate(WorldRule rule, WorldProgress progress) {
|
||||
TriggerSettings settings = rule.triggers();
|
||||
if (!rule.enabled()) {
|
||||
return TriggerDecision.none("world disabled");
|
||||
}
|
||||
if (progress.paused()) {
|
||||
return TriggerDecision.none("paused");
|
||||
}
|
||||
if (rule.manualOnly() || settings.manualOnly()) {
|
||||
return TriggerDecision.none("manual only");
|
||||
}
|
||||
|
||||
boolean time = timeDue(settings, progress);
|
||||
boolean unique = uniqueDue(settings, progress);
|
||||
boolean online = onlineDue(settings, progress);
|
||||
boolean active = activeDue(settings, progress);
|
||||
|
||||
return switch (settings.mode()) {
|
||||
case TIME -> decision(time, ExpansionReason.TIME, describeTrigger(rule, progress));
|
||||
case UNIQUE_PLAYERS -> decision(unique, ExpansionReason.UNIQUE_PLAYERS, describeTrigger(rule, progress));
|
||||
case ONLINE_PLAYERS -> decision(online, ExpansionReason.ONLINE_PLAYERS, describeTrigger(rule, progress));
|
||||
case ACTIVE_PLAYERS -> decision(active, ExpansionReason.ACTIVE_PLAYERS, describeTrigger(rule, progress));
|
||||
case TIME_OR_UNIQUE_PLAYERS -> decision(time || unique, time && unique ? ExpansionReason.HYBRID : (time ? ExpansionReason.TIME : ExpansionReason.UNIQUE_PLAYERS), describeTrigger(rule, progress));
|
||||
case TIME_AND_UNIQUE_PLAYERS -> decision(time && unique, ExpansionReason.HYBRID, describeTrigger(rule, progress));
|
||||
case TIME_OR_ONLINE_PLAYERS -> decision(time || online, time && online ? ExpansionReason.HYBRID : (time ? ExpansionReason.TIME : ExpansionReason.ONLINE_PLAYERS), describeTrigger(rule, progress));
|
||||
case TIME_AND_ONLINE_PLAYERS -> decision(time && online, ExpansionReason.HYBRID, describeTrigger(rule, progress));
|
||||
case TIME_OR_ACTIVE_PLAYERS -> decision(time || active, time && active ? ExpansionReason.HYBRID : (time ? ExpansionReason.TIME : ExpansionReason.ACTIVE_PLAYERS), describeTrigger(rule, progress));
|
||||
case TIME_AND_ACTIVE_PLAYERS -> decision(time && active, ExpansionReason.HYBRID, describeTrigger(rule, progress));
|
||||
case MANUAL -> TriggerDecision.none("manual only");
|
||||
};
|
||||
}
|
||||
|
||||
public String describeTrigger(WorldRule rule, WorldProgress progress) {
|
||||
TriggerSettings settings = rule.triggers();
|
||||
return switch (settings.mode()) {
|
||||
case MANUAL -> "manual only";
|
||||
case TIME -> describeTime(settings, progress);
|
||||
case UNIQUE_PLAYERS -> describeUnique(settings, progress);
|
||||
case ONLINE_PLAYERS -> describeOnline(settings);
|
||||
case ACTIVE_PLAYERS -> describeActive(settings);
|
||||
case TIME_OR_UNIQUE_PLAYERS -> describeTime(settings, progress) + " or " + describeUnique(settings, progress);
|
||||
case TIME_AND_UNIQUE_PLAYERS -> describeTime(settings, progress) + " and " + describeUnique(settings, progress);
|
||||
case TIME_OR_ONLINE_PLAYERS -> describeTime(settings, progress) + " or " + describeOnline(settings);
|
||||
case TIME_AND_ONLINE_PLAYERS -> describeTime(settings, progress) + " and " + describeOnline(settings);
|
||||
case TIME_OR_ACTIVE_PLAYERS -> describeTime(settings, progress) + " or " + describeActive(settings);
|
||||
case TIME_AND_ACTIVE_PLAYERS -> describeTime(settings, progress) + " and " + describeActive(settings);
|
||||
};
|
||||
}
|
||||
|
||||
public int missedTimeExpansions(WorldRule rule, WorldProgress progress) {
|
||||
TriggerSettings settings = rule.triggers();
|
||||
if (!settings.mode().usesTime() || settings.timeIntervalMillis() <= 0L || progress.lastExpansionAt() <= 0L) {
|
||||
return 0;
|
||||
}
|
||||
long elapsed = System.currentTimeMillis() - progress.lastExpansionAt();
|
||||
if (elapsed < settings.timeIntervalMillis()) {
|
||||
return 0;
|
||||
}
|
||||
return (int) Math.max(1L, elapsed / settings.timeIntervalMillis());
|
||||
}
|
||||
|
||||
private boolean timeDue(TriggerSettings settings, WorldProgress progress) {
|
||||
return settings.timeIntervalMillis() > 0L
|
||||
&& progress.lastExpansionAt() > 0L
|
||||
&& System.currentTimeMillis() >= progress.lastExpansionAt() + settings.timeIntervalMillis();
|
||||
}
|
||||
|
||||
private boolean uniqueDue(TriggerSettings settings, WorldProgress progress) {
|
||||
if (settings.uniquePlayersEvery() <= 0) {
|
||||
return false;
|
||||
}
|
||||
return playerStatsManager.uniquePlayerCount() >= progress.uniquePlayersAtLastExpansion() + settings.uniquePlayersEvery();
|
||||
}
|
||||
|
||||
private boolean onlineDue(TriggerSettings settings, WorldProgress progress) {
|
||||
if (settings.onlinePlayersThreshold() <= 0) {
|
||||
return false;
|
||||
}
|
||||
return playerStatsManager.onlinePlayerCount(settings.excludeVanished()) >= settings.onlinePlayersThreshold()
|
||||
&& playerCooldownPassed(settings, progress);
|
||||
}
|
||||
|
||||
private boolean activeDue(TriggerSettings settings, WorldProgress progress) {
|
||||
if (settings.activePlayersThreshold() <= 0) {
|
||||
return false;
|
||||
}
|
||||
return playerStatsManager.activePlayerCount(settings.activeWindowMillis()) >= settings.activePlayersThreshold()
|
||||
&& playerCooldownPassed(settings, progress);
|
||||
}
|
||||
|
||||
private boolean playerCooldownPassed(TriggerSettings settings, WorldProgress progress) {
|
||||
return settings.playerTriggerCooldownMillis() <= 0L
|
||||
|| progress.lastExpansionAt() <= 0L
|
||||
|| System.currentTimeMillis() >= progress.lastExpansionAt() + settings.playerTriggerCooldownMillis();
|
||||
}
|
||||
|
||||
private TriggerDecision decision(boolean due, ExpansionReason reason, String description) {
|
||||
return due ? new TriggerDecision(true, reason, description) : TriggerDecision.none(description);
|
||||
}
|
||||
|
||||
private String describeTime(TriggerSettings settings, WorldProgress progress) {
|
||||
long next = progress.lastExpansionAt() + settings.timeIntervalMillis();
|
||||
long remaining = next - System.currentTimeMillis();
|
||||
return remaining <= 0L ? "time due" : "time in " + TimeUtil.formatDuration(remaining);
|
||||
}
|
||||
|
||||
private String describeUnique(TriggerSettings settings, WorldProgress progress) {
|
||||
int current = playerStatsManager.uniquePlayerCount();
|
||||
int needed = progress.uniquePlayersAtLastExpansion() + settings.uniquePlayersEvery();
|
||||
return "unique players " + current + "/" + needed;
|
||||
}
|
||||
|
||||
private String describeOnline(TriggerSettings settings) {
|
||||
int current = playerStatsManager.onlinePlayerCount(settings.excludeVanished());
|
||||
return "online players " + current + "/" + settings.onlinePlayersThreshold();
|
||||
}
|
||||
|
||||
private String describeActive(TriggerSettings settings) {
|
||||
int current = playerStatsManager.activePlayerCount(settings.activeWindowMillis());
|
||||
return "active players " + current + "/" + settings.activePlayersThreshold();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.dirtsmp.dirtsmp.service;
|
||||
|
||||
import com.dirtsmp.dirtsmp.DirtSMPPlugin;
|
||||
import com.dirtsmp.dirtsmp.config.ConfigManager;
|
||||
import com.dirtsmp.dirtsmp.config.MessageManager;
|
||||
import com.dirtsmp.dirtsmp.model.ExpansionReason;
|
||||
import com.dirtsmp.dirtsmp.model.PhaseDefinition;
|
||||
import com.dirtsmp.dirtsmp.model.WorldRule;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
import org.bukkit.Bukkit;
|
||||
|
||||
public final class WebhookNotifier {
|
||||
private final DirtSMPPlugin plugin;
|
||||
private final ConfigManager configManager;
|
||||
private final MessageManager messageManager;
|
||||
|
||||
public WebhookNotifier(DirtSMPPlugin plugin, ConfigManager configManager, MessageManager messageManager) {
|
||||
this.plugin = plugin;
|
||||
this.configManager = configManager;
|
||||
this.messageManager = messageManager;
|
||||
}
|
||||
|
||||
public void notifyExpansion(WorldRule rule, double oldSize, double newSize, ExpansionReason reason, String actor, PhaseDefinition phase) {
|
||||
if (!configManager.webhookEnabled() || configManager.webhookUrl().isBlank()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, String> placeholders = messageManager.basePlaceholders(rule, oldSize, newSize, reason, actor, phase);
|
||||
String content = messageManager.applyPlaceholders(configManager.webhookContent(), placeholders);
|
||||
String body = "{\"content\":\"" + escapeJson(content) + "\"}";
|
||||
String url = configManager.webhookUrl();
|
||||
int timeoutSeconds = configManager.webhookTimeoutSeconds();
|
||||
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
try {
|
||||
HttpClient client = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(timeoutSeconds))
|
||||
.build();
|
||||
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
|
||||
.timeout(Duration.ofSeconds(timeoutSeconds))
|
||||
.header("Content-Type", "application/json")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(body))
|
||||
.build();
|
||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
if (response.statusCode() < 200 || response.statusCode() >= 300) {
|
||||
plugin.getLogger().warning("Webhook returned HTTP " + response.statusCode() + ".");
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
plugin.getLogger().log(Level.WARNING, "Could not send DirtSMP webhook.", ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String escapeJson(String input) {
|
||||
return input
|
||||
.replace("\\", "\\\\")
|
||||
.replace("\"", "\\\"")
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package com.dirtsmp.dirtsmp.state;
|
||||
|
||||
import com.dirtsmp.dirtsmp.DirtSMPPlugin;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.metadata.MetadataValue;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
|
||||
public final class PlayerStatsManager {
|
||||
private final DirtSMPPlugin plugin;
|
||||
private final Map<UUID, PlayerRecord> players = new HashMap<>();
|
||||
private File file;
|
||||
|
||||
public PlayerStatsManager(DirtSMPPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public void load() {
|
||||
file = new File(plugin.getDataFolder(), "players.yml");
|
||||
if (!file.exists()) {
|
||||
try {
|
||||
file.createNewFile();
|
||||
} catch (IOException ex) {
|
||||
plugin.getLogger().log(Level.WARNING, "Could not create players.yml.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
players.clear();
|
||||
YamlConfiguration config = YamlConfiguration.loadConfiguration(file);
|
||||
ConfigurationSection section = config.getConfigurationSection("players");
|
||||
if (section != null) {
|
||||
for (String rawUuid : section.getKeys(false)) {
|
||||
try {
|
||||
UUID uuid = UUID.fromString(rawUuid);
|
||||
String path = "players." + rawUuid + ".";
|
||||
players.put(uuid, new PlayerRecord(
|
||||
config.getString(path + "name", "unknown"),
|
||||
config.getLong(path + "first-seen", 0L),
|
||||
config.getLong(path + "last-seen", 0L)
|
||||
));
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
plugin.getLogger().warning("Ignoring invalid UUID in players.yml: " + rawUuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Bukkit.getOnlinePlayers().forEach(this::markSeen);
|
||||
}
|
||||
|
||||
public void markSeen(Player player) {
|
||||
long now = System.currentTimeMillis();
|
||||
players.compute(player.getUniqueId(), (uuid, existing) -> {
|
||||
if (existing == null) {
|
||||
return new PlayerRecord(player.getName(), now, now);
|
||||
}
|
||||
existing.name = player.getName();
|
||||
existing.lastSeen = now;
|
||||
if (existing.firstSeen <= 0L) {
|
||||
existing.firstSeen = now;
|
||||
}
|
||||
return existing;
|
||||
});
|
||||
}
|
||||
|
||||
public int uniquePlayerCount() {
|
||||
return players.size();
|
||||
}
|
||||
|
||||
public int onlinePlayerCount(boolean excludeVanished) {
|
||||
int count = 0;
|
||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||
if (!excludeVanished || !isVanished(player)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
public int activePlayerCount(long windowMillis) {
|
||||
long cutoff = System.currentTimeMillis() - Math.max(0L, windowMillis);
|
||||
int count = 0;
|
||||
for (PlayerRecord record : players.values()) {
|
||||
if (record.lastSeen >= cutoff) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private boolean isVanished(Player player) {
|
||||
for (MetadataValue value : player.getMetadata("vanished")) {
|
||||
if (value.asBoolean()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void saveQuietly() {
|
||||
try {
|
||||
save();
|
||||
} catch (IOException ex) {
|
||||
plugin.getLogger().log(Level.WARNING, "Could not save player stats.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void save() throws IOException {
|
||||
if (file == null) {
|
||||
file = new File(plugin.getDataFolder(), "players.yml");
|
||||
}
|
||||
YamlConfiguration config = new YamlConfiguration();
|
||||
for (Map.Entry<UUID, PlayerRecord> entry : players.entrySet()) {
|
||||
String path = "players." + entry.getKey() + ".";
|
||||
PlayerRecord record = entry.getValue();
|
||||
config.set(path + "name", record.name);
|
||||
config.set(path + "first-seen", record.firstSeen);
|
||||
config.set(path + "last-seen", record.lastSeen);
|
||||
}
|
||||
config.save(file);
|
||||
}
|
||||
|
||||
public void clearRuntimeState() {
|
||||
players.clear();
|
||||
}
|
||||
|
||||
private static final class PlayerRecord {
|
||||
private String name;
|
||||
private long firstSeen;
|
||||
private long lastSeen;
|
||||
|
||||
private PlayerRecord(String name, long firstSeen, long lastSeen) {
|
||||
this.name = name;
|
||||
this.firstSeen = firstSeen;
|
||||
this.lastSeen = lastSeen;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
package com.dirtsmp.dirtsmp.state;
|
||||
|
||||
import com.dirtsmp.dirtsmp.DirtSMPPlugin;
|
||||
import com.dirtsmp.dirtsmp.config.ConfigManager;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
|
||||
public final class StateManager {
|
||||
private static final DateTimeFormatter BACKUP_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss");
|
||||
|
||||
private final DirtSMPPlugin plugin;
|
||||
private final ConfigManager configManager;
|
||||
private final Map<String, WorldProgress> progressByWorld = new HashMap<>();
|
||||
private final Map<UUID, LegacyUserRecord> legacyUsers = new LinkedHashMap<>();
|
||||
private File stateFile;
|
||||
|
||||
public StateManager(DirtSMPPlugin plugin, ConfigManager configManager) {
|
||||
this.plugin = plugin;
|
||||
this.configManager = configManager;
|
||||
}
|
||||
|
||||
public void load() {
|
||||
stateFile = new File(plugin.getDataFolder(), configManager.stateFileName());
|
||||
if (!stateFile.exists()) {
|
||||
try {
|
||||
stateFile.createNewFile();
|
||||
} catch (IOException ex) {
|
||||
plugin.getLogger().log(Level.WARNING, "Could not create state file " + stateFile.getName(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
progressByWorld.clear();
|
||||
legacyUsers.clear();
|
||||
YamlConfiguration state = YamlConfiguration.loadConfiguration(stateFile);
|
||||
ConfigurationSection worlds = state.getConfigurationSection("worlds");
|
||||
if (worlds != null) {
|
||||
for (String key : worlds.getKeys(false)) {
|
||||
ConfigurationSection section = worlds.getConfigurationSection(key);
|
||||
if (section == null) {
|
||||
continue;
|
||||
}
|
||||
WorldProgress progress = new WorldProgress();
|
||||
progress.initialized(section.getBoolean("initialized", false));
|
||||
progress.currentSize(section.getDouble("current-size", 0.0));
|
||||
progress.expansionCount(section.getInt("expansion-count", 0));
|
||||
progress.currentPhaseIndex(section.getInt("current-phase-index", 0));
|
||||
progress.paused(section.getBoolean("paused", false));
|
||||
progress.lastExpansionAt(section.getLong("last-expansion-at", 0L));
|
||||
progress.uniquePlayersAtLastExpansion(section.getInt("unique-players-at-last-expansion", 0));
|
||||
progress.sentReminders(new HashSet<>(section.getStringList("sent-reminders")));
|
||||
progressByWorld.put(key.toLowerCase(), progress);
|
||||
}
|
||||
}
|
||||
|
||||
ConfigurationSection legacySection = state.getConfigurationSection("legacy-users");
|
||||
if (legacySection != null) {
|
||||
for (String rawUuid : legacySection.getKeys(false)) {
|
||||
try {
|
||||
UUID uuid = UUID.fromString(rawUuid);
|
||||
String path = "legacy-users." + rawUuid + ".";
|
||||
legacyUsers.put(uuid, new LegacyUserRecord(
|
||||
state.getString(path + "name", "unknown"),
|
||||
state.getLong(path + "playtime-ticks", 0L),
|
||||
state.getLong(path + "added-at", 0L),
|
||||
state.getString(path + "source", "unknown")
|
||||
));
|
||||
} catch (IllegalArgumentException ex) {
|
||||
plugin.getLogger().warning("Ignoring invalid legacy user UUID in state.yml: " + rawUuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public WorldProgress progress(String key) {
|
||||
return progressByWorld.computeIfAbsent(key.toLowerCase(), ignored -> new WorldProgress());
|
||||
}
|
||||
|
||||
public Map<String, WorldProgress> allProgress() {
|
||||
return progressByWorld;
|
||||
}
|
||||
|
||||
public boolean isLegacyUser(UUID uuid) {
|
||||
return legacyUsers.containsKey(uuid);
|
||||
}
|
||||
|
||||
public Map<UUID, LegacyUserRecord> legacyUsers() {
|
||||
return legacyUsers;
|
||||
}
|
||||
|
||||
public boolean addLegacyUser(UUID uuid, String name, long playtimeTicks, String source) {
|
||||
boolean added = !legacyUsers.containsKey(uuid);
|
||||
legacyUsers.put(uuid, new LegacyUserRecord(
|
||||
name == null || name.isBlank() ? uuid.toString() : name,
|
||||
Math.max(0L, playtimeTicks),
|
||||
System.currentTimeMillis(),
|
||||
source == null || source.isBlank() ? "manual" : source
|
||||
));
|
||||
return added;
|
||||
}
|
||||
|
||||
public boolean removeLegacyUser(UUID uuid) {
|
||||
return legacyUsers.remove(uuid) != null;
|
||||
}
|
||||
|
||||
public void saveQuietly() {
|
||||
try {
|
||||
save();
|
||||
} catch (IOException ex) {
|
||||
plugin.getLogger().log(Level.WARNING, "Could not save DirtSMP state.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void save() throws IOException {
|
||||
if (stateFile == null) {
|
||||
stateFile = new File(plugin.getDataFolder(), configManager.stateFileName());
|
||||
}
|
||||
if (configManager.backupStateOnSave() && stateFile.exists() && stateFile.length() > 0L) {
|
||||
backupState();
|
||||
}
|
||||
|
||||
YamlConfiguration state = new YamlConfiguration();
|
||||
for (Map.Entry<String, WorldProgress> entry : progressByWorld.entrySet()) {
|
||||
String path = "worlds." + entry.getKey() + ".";
|
||||
WorldProgress progress = entry.getValue();
|
||||
state.set(path + "initialized", progress.initialized());
|
||||
state.set(path + "current-size", progress.currentSize());
|
||||
state.set(path + "expansion-count", progress.expansionCount());
|
||||
state.set(path + "current-phase-index", progress.currentPhaseIndex());
|
||||
state.set(path + "paused", progress.paused());
|
||||
state.set(path + "last-expansion-at", progress.lastExpansionAt());
|
||||
state.set(path + "unique-players-at-last-expansion", progress.uniquePlayersAtLastExpansion());
|
||||
state.set(path + "sent-reminders", progress.sentReminders().stream().sorted().toList());
|
||||
}
|
||||
for (Map.Entry<UUID, LegacyUserRecord> entry : legacyUsers.entrySet()) {
|
||||
String path = "legacy-users." + entry.getKey() + ".";
|
||||
LegacyUserRecord record = entry.getValue();
|
||||
state.set(path + "name", record.name());
|
||||
state.set(path + "playtime-ticks", record.playtimeTicks());
|
||||
state.set(path + "added-at", record.addedAt());
|
||||
state.set(path + "source", record.source());
|
||||
}
|
||||
state.save(stateFile);
|
||||
}
|
||||
|
||||
private void backupState() {
|
||||
int keep = configManager.backupKeep();
|
||||
if (keep <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
File backupDirectory = new File(plugin.getDataFolder(), "backups");
|
||||
if (!backupDirectory.exists() && !backupDirectory.mkdirs()) {
|
||||
plugin.getLogger().warning("Could not create backups directory for state file backups.");
|
||||
return;
|
||||
}
|
||||
|
||||
File backup = new File(backupDirectory, "state-" + LocalDateTime.now().format(BACKUP_FORMAT) + ".yml");
|
||||
try {
|
||||
Files.copy(stateFile.toPath(), backup.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
pruneBackups(backupDirectory, keep);
|
||||
} catch (IOException ex) {
|
||||
plugin.getLogger().log(Level.WARNING, "Could not back up state file.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void pruneBackups(File backupDirectory, int keep) throws IOException {
|
||||
File[] files = backupDirectory.listFiles((dir, name) -> name.startsWith("state-") && name.endsWith(".yml"));
|
||||
if (files == null || files.length <= keep) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (File file : java.util.Arrays.stream(files)
|
||||
.sorted(Comparator.comparingLong(File::lastModified).reversed())
|
||||
.skip(keep)
|
||||
.toList()) {
|
||||
Files.deleteIfExists(file.toPath());
|
||||
}
|
||||
}
|
||||
|
||||
public void clearRuntimeState() {
|
||||
progressByWorld.clear();
|
||||
legacyUsers.clear();
|
||||
}
|
||||
|
||||
public record LegacyUserRecord(
|
||||
String name,
|
||||
long playtimeTicks,
|
||||
long addedAt,
|
||||
String source
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.dirtsmp.dirtsmp.state;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public final class WorldProgress {
|
||||
private boolean initialized;
|
||||
private double currentSize;
|
||||
private int expansionCount;
|
||||
private int currentPhaseIndex;
|
||||
private boolean paused;
|
||||
private long lastExpansionAt;
|
||||
private int uniquePlayersAtLastExpansion;
|
||||
private Set<String> sentReminders = new HashSet<>();
|
||||
|
||||
public boolean initialized() {
|
||||
return initialized;
|
||||
}
|
||||
|
||||
public void initialized(boolean initialized) {
|
||||
this.initialized = initialized;
|
||||
}
|
||||
|
||||
public double currentSize() {
|
||||
return currentSize;
|
||||
}
|
||||
|
||||
public void currentSize(double currentSize) {
|
||||
this.currentSize = currentSize;
|
||||
}
|
||||
|
||||
public int expansionCount() {
|
||||
return expansionCount;
|
||||
}
|
||||
|
||||
public void expansionCount(int expansionCount) {
|
||||
this.expansionCount = expansionCount;
|
||||
}
|
||||
|
||||
public int currentPhaseIndex() {
|
||||
return currentPhaseIndex;
|
||||
}
|
||||
|
||||
public void currentPhaseIndex(int currentPhaseIndex) {
|
||||
this.currentPhaseIndex = currentPhaseIndex;
|
||||
}
|
||||
|
||||
public boolean paused() {
|
||||
return paused;
|
||||
}
|
||||
|
||||
public void paused(boolean paused) {
|
||||
this.paused = paused;
|
||||
}
|
||||
|
||||
public long lastExpansionAt() {
|
||||
return lastExpansionAt;
|
||||
}
|
||||
|
||||
public void lastExpansionAt(long lastExpansionAt) {
|
||||
this.lastExpansionAt = lastExpansionAt;
|
||||
}
|
||||
|
||||
public int uniquePlayersAtLastExpansion() {
|
||||
return uniquePlayersAtLastExpansion;
|
||||
}
|
||||
|
||||
public void uniquePlayersAtLastExpansion(int uniquePlayersAtLastExpansion) {
|
||||
this.uniquePlayersAtLastExpansion = uniquePlayersAtLastExpansion;
|
||||
}
|
||||
|
||||
public Set<String> sentReminders() {
|
||||
return sentReminders;
|
||||
}
|
||||
|
||||
public void sentReminders(Set<String> sentReminders) {
|
||||
this.sentReminders = new HashSet<>(sentReminders);
|
||||
}
|
||||
|
||||
public boolean markReminderSent(String reminderKey) {
|
||||
return sentReminders.add(reminderKey);
|
||||
}
|
||||
|
||||
public void clearReminders() {
|
||||
sentReminders.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package com.dirtsmp.dirtsmp.task;
|
||||
|
||||
import com.dirtsmp.dirtsmp.DirtSMPPlugin;
|
||||
import com.dirtsmp.dirtsmp.config.ConfigManager;
|
||||
import com.dirtsmp.dirtsmp.config.MessageManager;
|
||||
import com.dirtsmp.dirtsmp.model.CatchUpMode;
|
||||
import com.dirtsmp.dirtsmp.model.ExpansionReason;
|
||||
import com.dirtsmp.dirtsmp.model.TriggerDecision;
|
||||
import com.dirtsmp.dirtsmp.model.TriggerMode;
|
||||
import com.dirtsmp.dirtsmp.model.WorldRule;
|
||||
import com.dirtsmp.dirtsmp.service.BorderManager;
|
||||
import com.dirtsmp.dirtsmp.service.TriggerEvaluator;
|
||||
import com.dirtsmp.dirtsmp.state.PlayerStatsManager;
|
||||
import com.dirtsmp.dirtsmp.state.StateManager;
|
||||
import com.dirtsmp.dirtsmp.state.WorldProgress;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
public final class ScheduleManager {
|
||||
private final DirtSMPPlugin plugin;
|
||||
private final ConfigManager configManager;
|
||||
private final StateManager stateManager;
|
||||
private final PlayerStatsManager playerStatsManager;
|
||||
private final BorderManager borderManager;
|
||||
private final TriggerEvaluator triggerEvaluator;
|
||||
private final MessageManager messageManager;
|
||||
private BukkitTask pollTask;
|
||||
private BukkitTask saveTask;
|
||||
|
||||
public ScheduleManager(
|
||||
DirtSMPPlugin plugin,
|
||||
ConfigManager configManager,
|
||||
StateManager stateManager,
|
||||
PlayerStatsManager playerStatsManager,
|
||||
BorderManager borderManager,
|
||||
TriggerEvaluator triggerEvaluator,
|
||||
MessageManager messageManager
|
||||
) {
|
||||
this.plugin = plugin;
|
||||
this.configManager = configManager;
|
||||
this.stateManager = stateManager;
|
||||
this.playerStatsManager = playerStatsManager;
|
||||
this.borderManager = borderManager;
|
||||
this.triggerEvaluator = triggerEvaluator;
|
||||
this.messageManager = messageManager;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
stop();
|
||||
applyCatchUp();
|
||||
pollTask = Bukkit.getScheduler().runTaskTimer(plugin, this::tick, configManager.pollIntervalTicks(), configManager.pollIntervalTicks());
|
||||
saveTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> {
|
||||
stateManager.saveQuietly();
|
||||
playerStatsManager.saveQuietly();
|
||||
}, configManager.saveIntervalTicks(), configManager.saveIntervalTicks());
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (pollTask != null) {
|
||||
pollTask.cancel();
|
||||
pollTask = null;
|
||||
}
|
||||
if (saveTask != null) {
|
||||
saveTask.cancel();
|
||||
saveTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void tick() {
|
||||
Bukkit.getOnlinePlayers().forEach(playerStatsManager::markSeen);
|
||||
handleReminders();
|
||||
handleTriggers();
|
||||
}
|
||||
|
||||
private void handleTriggers() {
|
||||
for (WorldRule rule : configManager.worlds().values()) {
|
||||
WorldProgress progress = stateManager.progress(rule.key());
|
||||
TriggerDecision decision = triggerEvaluator.evaluate(rule, progress);
|
||||
if (decision.due()) {
|
||||
borderManager.expand(rule, decision.reason(), "automatic", false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleReminders() {
|
||||
long now = System.currentTimeMillis();
|
||||
for (WorldRule rule : configManager.worlds().values()) {
|
||||
if (!rule.enabled() || !rule.reminders().enabled() || !rule.triggers().mode().usesTime()) {
|
||||
continue;
|
||||
}
|
||||
WorldProgress progress = stateManager.progress(rule.key());
|
||||
if (progress.paused() || progress.lastExpansionAt() <= 0L || rule.triggers().timeIntervalMillis() <= 0L) {
|
||||
continue;
|
||||
}
|
||||
|
||||
long next = progress.lastExpansionAt() + rule.triggers().timeIntervalMillis();
|
||||
if (now >= next) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (long reminderMillis : rule.reminders().beforeMillis()) {
|
||||
long reminderAt = next - reminderMillis;
|
||||
String key = next + ":" + reminderMillis;
|
||||
if (now >= reminderAt && progress.markReminderSent(key)) {
|
||||
messageManager.announceReminder(rule, next - now);
|
||||
stateManager.saveQuietly();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void applyCatchUp() {
|
||||
for (WorldRule rule : configManager.worlds().values()) {
|
||||
WorldProgress progress = stateManager.progress(rule.key());
|
||||
if (!rule.enabled() || progress.paused() || rule.manualOnly() || rule.triggers().mode() == TriggerMode.MANUAL) {
|
||||
continue;
|
||||
}
|
||||
if (rule.triggers().catchUpMode() == CatchUpMode.NONE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
TriggerDecision decision = triggerEvaluator.evaluate(rule, progress);
|
||||
if (!decision.due() || !rule.triggers().mode().usesTime()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int missed = triggerEvaluator.missedTimeExpansions(rule, progress);
|
||||
if (missed <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int amount = 1;
|
||||
if (rule.triggers().catchUpMode() == CatchUpMode.ALL && rule.triggers().mode() == TriggerMode.TIME) {
|
||||
amount = Math.min(missed, configManager.maxCatchupExpansionsPerWorld());
|
||||
}
|
||||
|
||||
for (int index = 0; index < amount; index++) {
|
||||
if (!borderManager.expand(rule, ExpansionReason.CATCH_UP, "startup", false).success()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,414 @@
|
||||
# DirtbagMC progression configuration
|
||||
#
|
||||
# Size terminology:
|
||||
# Every border size in this file is a FULL DIAMETER / WIDTH in blocks, matching Bukkit/Paper's WorldBorder size.
|
||||
# Example: starting-size: 50000 creates a border that is 50,000 blocks wide, roughly 25,000 blocks from center to edge.
|
||||
#
|
||||
# Growth modes:
|
||||
# INCREMENTAL - each expansion adds growth-amount to the current full diameter.
|
||||
# PHASE - each expansion moves to the next exact size listed under phases.
|
||||
#
|
||||
# Border modes:
|
||||
# WORLD_BORDER - use Minecraft/Paper's real WorldBorder.
|
||||
# SOFT_BORDER - do not enforce the vanilla border; bounce players off an invisible DirtbagMC border with effects.
|
||||
#
|
||||
# Trigger modes:
|
||||
# MANUAL, TIME, UNIQUE_PLAYERS, ONLINE_PLAYERS, ACTIVE_PLAYERS,
|
||||
# TIME_OR_UNIQUE_PLAYERS, TIME_AND_UNIQUE_PLAYERS,
|
||||
# TIME_OR_ONLINE_PLAYERS, TIME_AND_ONLINE_PLAYERS,
|
||||
# TIME_OR_ACTIVE_PLAYERS, TIME_AND_ACTIVE_PLAYERS.
|
||||
|
||||
settings:
|
||||
dry-run: false
|
||||
debug: false
|
||||
poll-interval-seconds: 30
|
||||
apply-borders-on-startup: true
|
||||
max-catchup-expansions-per-world: 20
|
||||
|
||||
state:
|
||||
file-name: state.yml
|
||||
save-interval-seconds: 300
|
||||
backup-on-save: true
|
||||
backup-keep: 10
|
||||
|
||||
legacy-users:
|
||||
# Admin scan command:
|
||||
# /dirtsmp legacy scan
|
||||
# Also supported:
|
||||
# /dirtsmp add legacy users scan
|
||||
# Players with at least this much Minecraft playtime are added to state.yml under legacy-users.
|
||||
minimum-playtime: 1h
|
||||
|
||||
history:
|
||||
enabled: true
|
||||
file-name: history.log
|
||||
|
||||
gui:
|
||||
enabled: true
|
||||
title: "A2416&lᴅA2D1B&lɪA351F&lʀA3F24&lᴛA4A2A&lʙB6B35&lᴀ&#A8873F&lɢ&#D4AF37&lᴍ&#B9C63F&lᴄ &8| &#D4AF37&lBorders"
|
||||
|
||||
webhook:
|
||||
enabled: false
|
||||
url: ""
|
||||
timeout-seconds: 8
|
||||
content: "**DirtbagMC progression:** `{world}` expanded from `{old_size}` to `{new_size}` blocks. Reason: `{reason}`"
|
||||
|
||||
command-hooks:
|
||||
enabled: true
|
||||
before-expansion:
|
||||
# - "say Preparing expansion for {world} to {new_size}"
|
||||
after-expansion:
|
||||
# Useful for tools like Chunky:
|
||||
# - "chunky world {world}"
|
||||
# - "chunky border"
|
||||
# - "chunky start"
|
||||
|
||||
milestone-rewards:
|
||||
enabled: true
|
||||
# Commands run from console after an expansion reaches a configured milestone.
|
||||
# Placeholders: {world}, {key}, {old_size}, {new_size}, {max_size}, {reason}, {actor}, {phase}, {phase_name}, {expansion_count}
|
||||
every-expansion: []
|
||||
by-expansion-count:
|
||||
# Example rewards:
|
||||
# 1:
|
||||
# - "broadcast DirtbagMC milestone 1 reached in {world}!"
|
||||
# - "crate key giveall beta 1"
|
||||
1: []
|
||||
3: []
|
||||
5: []
|
||||
by-phase-index:
|
||||
1: []
|
||||
by-phase-name:
|
||||
Frontier Era: []
|
||||
First Ring: []
|
||||
|
||||
worlds:
|
||||
overworld:
|
||||
enabled: true
|
||||
world: world
|
||||
center-x: 0.0
|
||||
center-z: 0.0
|
||||
starting-size: 50000
|
||||
border-mode: SOFT_BORDER
|
||||
growth-mode: INCREMENTAL
|
||||
growth-amount: 10000
|
||||
max-size: 200000
|
||||
transition-seconds: 300
|
||||
manual-only: false
|
||||
import-current-border: false
|
||||
no-shrink-protection: true
|
||||
enforce-max-size: true
|
||||
command-hooks:
|
||||
before-expansion: []
|
||||
after-expansion: []
|
||||
milestone-rewards:
|
||||
enabled: true
|
||||
every-expansion: []
|
||||
by-expansion-count:
|
||||
1: []
|
||||
2: []
|
||||
3: []
|
||||
by-phase-index: {}
|
||||
by-phase-name: {}
|
||||
legacy-access:
|
||||
enabled: true
|
||||
# This protects beta builds from being stranded when progression starts after launch.
|
||||
# The plugin expands the initial/saved border enough to include known locations plus padding.
|
||||
include-current-border: false
|
||||
include-online-players: true
|
||||
include-offline-last-locations: true
|
||||
include-respawn-locations: true
|
||||
# When true, player last/respawn locations only count if the player is in the scanned legacy-users list.
|
||||
player-locations-require-legacy-user: true
|
||||
reconcile-on-startup: true
|
||||
allow-start-above-max-size: true
|
||||
padding: 1024
|
||||
# Add homes, towns, bases, or Essentials homes that cannot be discovered from Paper playerdata.
|
||||
# Border math uses full diameter/width, centered on center-x/center-z.
|
||||
locations: []
|
||||
# Example:
|
||||
# locations:
|
||||
# - name: "Beta town"
|
||||
# x: 31500
|
||||
# z: -22400
|
||||
soft-border:
|
||||
# Premium invisible border mode. Used only when border-mode is SOFT_BORDER.
|
||||
# release-vanilla-border removes any old vanilla border enforcement by setting it to vanilla-border-size.
|
||||
release-vanilla-border: true
|
||||
vanilla-border-size: 59999968
|
||||
ignore-creative: false
|
||||
ignore-spectator: true
|
||||
bypass-permission: dirtsmp.bypass.softborder
|
||||
inside-buffer: 1.5
|
||||
bounce-strength: 1.85
|
||||
vertical-boost: 0.35
|
||||
protect-mounted-entities: true
|
||||
# Gentler values keep horses, camels, boats, and minecarts from taking weird launch angles.
|
||||
mounted-bounce-strength: 1.15
|
||||
mounted-vertical-boost: 0.12
|
||||
max-outside-distance-before-teleport: 24
|
||||
cooldown: 900ms
|
||||
message: "{prefix}&#D4AF37&lThe DirtbagMC border &7throws you back."
|
||||
action-bar: "&#D4AF37&lBorder reached &8| &7turn back"
|
||||
sound: ENTITY_SLIME_JUMP
|
||||
volume: 0.65
|
||||
pitch: 0.65
|
||||
particles:
|
||||
enabled: true
|
||||
# DUST uses the DirtbagMC gold color. Other Bukkit particle names also work.
|
||||
particle: DUST
|
||||
count: 3
|
||||
offset: 0.05
|
||||
speed: 0.0
|
||||
interval-ticks: 10
|
||||
view-distance: 96
|
||||
spacing: 3
|
||||
broadcasts:
|
||||
enabled: true
|
||||
world-only: false
|
||||
message: "{prefix}&#B9C63FThe dirt expands. &#D4AF37&l{world} &#B9C63Fhas grown from &#A8873F{old_size} &#B9C63Fto &#D4AF37&l{new_size} &#B9C63Fblocks."
|
||||
title: "A2416&lᴅA2D1B&lɪA351F&lʀA3F24&lᴛA4A2A&lʙB6B35&lᴀ&#A8873F&lɢ&#D4AF37&lᴍ&#B9C63F&lᴄ"
|
||||
subtitle: "&#B9C63F{world} &8| &#D4AF37{old_size} &7-> &#D4AF37{new_size}"
|
||||
action-bar: "&#D4AF37&l{world} &7border is now &#B9C63F{new_size} &7blocks wide"
|
||||
sound: ENTITY_ENDER_DRAGON_GROWL
|
||||
volume: 0.7
|
||||
pitch: 1.25
|
||||
reminders:
|
||||
enabled: true
|
||||
before:
|
||||
- 1d
|
||||
- 12h
|
||||
- 1h
|
||||
- 10m
|
||||
message: "{prefix}&#D4AF37&l{world} &7expands in &#B9C63F{time_left}&7."
|
||||
action-bar: "&#D4AF37&l{world} &7expands in &#B9C63F{time_left}"
|
||||
sound: BLOCK_NOTE_BLOCK_PLING
|
||||
volume: 0.45
|
||||
pitch: 1.4
|
||||
triggers:
|
||||
mode: TIME_OR_UNIQUE_PLAYERS
|
||||
time-interval: 7d
|
||||
unique-players-every: 50
|
||||
online-players-threshold: 35
|
||||
active-players-threshold: 120
|
||||
active-window: 7d
|
||||
exclude-vanished: true
|
||||
player-trigger-cooldown: 12h
|
||||
catch-up: ONE
|
||||
phases:
|
||||
- name: Spawn Era
|
||||
size: 50000
|
||||
message: ""
|
||||
- name: Frontier Era
|
||||
size: 75000
|
||||
message: ""
|
||||
|
||||
end:
|
||||
enabled: true
|
||||
world: world_the_end
|
||||
center-x: 0.0
|
||||
center-z: 0.0
|
||||
starting-size: 2000
|
||||
border-mode: SOFT_BORDER
|
||||
growth-mode: PHASE
|
||||
growth-amount: 2000
|
||||
max-size: 20000
|
||||
transition-seconds: 180
|
||||
manual-only: false
|
||||
import-current-border: false
|
||||
no-shrink-protection: true
|
||||
enforce-max-size: true
|
||||
command-hooks:
|
||||
before-expansion: []
|
||||
after-expansion: []
|
||||
milestone-rewards:
|
||||
enabled: true
|
||||
every-expansion: []
|
||||
by-expansion-count:
|
||||
1: []
|
||||
2: []
|
||||
by-phase-index:
|
||||
1: []
|
||||
2: []
|
||||
by-phase-name:
|
||||
First Ring: []
|
||||
Chorus Frontier: []
|
||||
legacy-access:
|
||||
enabled: true
|
||||
include-current-border: false
|
||||
include-online-players: true
|
||||
include-offline-last-locations: true
|
||||
include-respawn-locations: true
|
||||
player-locations-require-legacy-user: true
|
||||
reconcile-on-startup: true
|
||||
allow-start-above-max-size: true
|
||||
padding: 512
|
||||
locations: []
|
||||
soft-border:
|
||||
release-vanilla-border: true
|
||||
vanilla-border-size: 59999968
|
||||
ignore-creative: false
|
||||
ignore-spectator: true
|
||||
bypass-permission: dirtsmp.bypass.softborder
|
||||
inside-buffer: 1.5
|
||||
bounce-strength: 2.05
|
||||
vertical-boost: 0.45
|
||||
protect-mounted-entities: true
|
||||
mounted-bounce-strength: 1.2
|
||||
mounted-vertical-boost: 0.12
|
||||
max-outside-distance-before-teleport: 18
|
||||
cooldown: 900ms
|
||||
message: "{prefix}A2416&lThe End border &7snaps you back."
|
||||
action-bar: "A2416&lThe End border &8| &7turn back"
|
||||
sound: ENTITY_ENDERMAN_TELEPORT
|
||||
volume: 0.55
|
||||
pitch: 0.8
|
||||
particles:
|
||||
enabled: true
|
||||
particle: DUST
|
||||
count: 3
|
||||
offset: 0.05
|
||||
speed: 0.0
|
||||
interval-ticks: 10
|
||||
view-distance: 72
|
||||
spacing: 2.5
|
||||
broadcasts:
|
||||
enabled: true
|
||||
world-only: false
|
||||
message: "{prefix}A2416&lThe End &#B9C63Fhas entered &#D4AF37&l{phase_name}&#B9C63F: &#D4AF37{new_size} &#B9C63Fblocks wide."
|
||||
title: "A2416&lThe End Opens"
|
||||
subtitle: "&#D4AF37{phase_name} &8| &#B9C63F{new_size} blocks"
|
||||
action-bar: "A2416&lThe End &7border is now &#D4AF37{new_size}"
|
||||
sound: ENTITY_ENDER_DRAGON_DEATH
|
||||
volume: 0.8
|
||||
pitch: 1.0
|
||||
reminders:
|
||||
enabled: true
|
||||
before:
|
||||
- 12h
|
||||
- 1h
|
||||
- 10m
|
||||
message: "{prefix}A2416&lThe End &7expands in &#D4AF37{time_left}&7."
|
||||
action-bar: "A2416&lThe End &7expands in &#D4AF37{time_left}"
|
||||
sound: BLOCK_NOTE_BLOCK_PLING
|
||||
volume: 0.45
|
||||
pitch: 1.6
|
||||
triggers:
|
||||
mode: TIME_AND_UNIQUE_PLAYERS
|
||||
time-interval: 10d
|
||||
unique-players-every: 100
|
||||
online-players-threshold: 40
|
||||
active-players-threshold: 150
|
||||
active-window: 7d
|
||||
exclude-vanished: true
|
||||
player-trigger-cooldown: 12h
|
||||
catch-up: ONE
|
||||
phases:
|
||||
- name: Outer Silence
|
||||
size: 2000
|
||||
message: ""
|
||||
- name: First Ring
|
||||
size: 4000
|
||||
message: "{prefix}A2416&lThe End &#B9C63Fstirs. &#D4AF37The first outer ring &#B9C63Fis now reachable."
|
||||
- name: Chorus Frontier
|
||||
size: 6000
|
||||
message: "{prefix}A2416&lThe End &#B9C63Fgrows again. &#D4AF37New islands await."
|
||||
- name: Dragon's Wake
|
||||
size: 8000
|
||||
message: "{prefix}A2416&lThe End &#B9C63Fborder has expanded to &#D4AF37{new_size} &#B9C63Fblocks."
|
||||
|
||||
nether:
|
||||
enabled: false
|
||||
world: world_nether
|
||||
center-x: 0.0
|
||||
center-z: 0.0
|
||||
starting-size: 10000
|
||||
border-mode: SOFT_BORDER
|
||||
growth-mode: INCREMENTAL
|
||||
growth-amount: 2500
|
||||
max-size: 50000
|
||||
transition-seconds: 120
|
||||
manual-only: false
|
||||
import-current-border: false
|
||||
no-shrink-protection: true
|
||||
enforce-max-size: true
|
||||
command-hooks:
|
||||
before-expansion: []
|
||||
after-expansion: []
|
||||
milestone-rewards:
|
||||
enabled: true
|
||||
every-expansion: []
|
||||
by-expansion-count:
|
||||
1: []
|
||||
by-phase-index: {}
|
||||
by-phase-name: {}
|
||||
legacy-access:
|
||||
enabled: false
|
||||
include-current-border: false
|
||||
include-online-players: true
|
||||
include-offline-last-locations: true
|
||||
include-respawn-locations: true
|
||||
player-locations-require-legacy-user: true
|
||||
reconcile-on-startup: true
|
||||
allow-start-above-max-size: true
|
||||
padding: 512
|
||||
locations: []
|
||||
soft-border:
|
||||
release-vanilla-border: true
|
||||
vanilla-border-size: 59999968
|
||||
ignore-creative: false
|
||||
ignore-spectator: true
|
||||
bypass-permission: dirtsmp.bypass.softborder
|
||||
inside-buffer: 1.5
|
||||
bounce-strength: 1.9
|
||||
vertical-boost: 0.35
|
||||
protect-mounted-entities: true
|
||||
mounted-bounce-strength: 1.15
|
||||
mounted-vertical-boost: 0.12
|
||||
max-outside-distance-before-teleport: 18
|
||||
cooldown: 900ms
|
||||
message: "{prefix}A4A2A&lThe Nether border &7throws you back."
|
||||
action-bar: "A4A2A&lNether border &8| &7turn back"
|
||||
sound: ENTITY_BLAZE_HURT
|
||||
volume: 0.55
|
||||
pitch: 0.8
|
||||
particles:
|
||||
enabled: true
|
||||
particle: DUST
|
||||
count: 3
|
||||
offset: 0.05
|
||||
speed: 0.0
|
||||
interval-ticks: 10
|
||||
view-distance: 72
|
||||
spacing: 2.5
|
||||
broadcasts:
|
||||
enabled: true
|
||||
world-only: false
|
||||
message: "{prefix}A4A2A&lThe Nether &#B9C63Fhas expanded to &#D4AF37{new_size} &#B9C63Fblocks."
|
||||
title: "A4A2A&lNether Expanded"
|
||||
subtitle: "&#A8873F{old_size} &7-> &#D4AF37{new_size}"
|
||||
action-bar: "A4A2A&lThe Nether &7border is now &#D4AF37{new_size}"
|
||||
sound: ENTITY_WITHER_SPAWN
|
||||
volume: 0.65
|
||||
pitch: 1.1
|
||||
reminders:
|
||||
enabled: true
|
||||
before:
|
||||
- 1h
|
||||
- 10m
|
||||
message: "{prefix}A4A2A&lThe Nether &7expands in &#D4AF37{time_left}&7."
|
||||
action-bar: "A4A2A&lThe Nether &7expands in &#D4AF37{time_left}"
|
||||
sound: BLOCK_NOTE_BLOCK_PLING
|
||||
volume: 0.45
|
||||
pitch: 1.2
|
||||
triggers:
|
||||
mode: MANUAL
|
||||
time-interval: 7d
|
||||
unique-players-every: 50
|
||||
online-players-threshold: 30
|
||||
active-players-threshold: 100
|
||||
active-window: 7d
|
||||
exclude-vanished: true
|
||||
player-trigger-cooldown: 12h
|
||||
catch-up: NONE
|
||||
phases: []
|
||||
@@ -0,0 +1,76 @@
|
||||
prefix: "A2416&lᴅA2D1B&lɪA351F&lʀA3F24&lᴛA4A2A&lʙB6B35&lᴀ&#A8873F&lɢ&#D4AF37&lᴍ&#B9C63F&lᴄ &8| &7"
|
||||
|
||||
commands:
|
||||
no-permission: "{prefix}&cYou do not have permission to do that."
|
||||
player-only: "{prefix}&cOnly players can use that command."
|
||||
unknown-world: "{prefix}&cUnknown configured world: &#D4AF37{world}"
|
||||
invalid-number: "{prefix}&cThat size must be a positive number."
|
||||
reload: "{prefix}&#B9C63FConfiguration, messages, and DirtbagMC progression rules reloaded."
|
||||
saved: "{prefix}&#B9C63FState saved."
|
||||
help:
|
||||
- "A2416&lᴅA2D1B&lɪA351F&lʀA3F24&lᴛA4A2A&lʙB6B35&lᴀ&#A8873F&lɢ&#D4AF37&lᴍ&#B9C63F&lᴄ &8| &#D4AF37&lBorder Progression"
|
||||
- "B6B35/dirtsmp status [world] &8- &7view progression"
|
||||
- "B6B35/dirtsmp next [world] &8- &7view upcoming triggers"
|
||||
- "B6B35/dirtsmp legacycheck <world> &8- &7scan beta home safety"
|
||||
- "B6B35/dirtsmp tpborder <world> [side] &8- &7teleport near a border wall"
|
||||
- "B6B35/dirtsmp legacy scan [1h] &8- &7mark legacy players by playtime"
|
||||
- "B6B35/dirtsmp legacy list &8- &7show marked legacy players"
|
||||
- "B6B35/dirtsmp expand <world> &8- &7expand now"
|
||||
- "B6B35/dirtsmp setsize <world> <size> [force] &8- &7set border size"
|
||||
- "B6B35/dirtsmp pause <world> &8- &7pause automatic progression"
|
||||
- "B6B35/dirtsmp resume <world> &8- &7resume automatic progression"
|
||||
- "B6B35/dirtsmp reset <world> [force] &8- &7reset progression"
|
||||
- "B6B35/dirtsmp gui &8- &7open admin panel"
|
||||
- "B6B35/dirtsmp reload &8- &7reload config"
|
||||
status-header: "{prefix}&#B9C63FTracking &#D4AF37&l{count} &#B9C63Fconfigured world(s)."
|
||||
status-line: "&8- &#D4AF37&l{key} &8(&7{world}&8) &7enabled: &#B9C63F{enabled} &7size: &#B9C63F{size} &7next: &#D4AF37{next_size} &7mode: &#A8873F{border_mode} &7phase: &#A8873F{phase} &7paused: &c{paused}"
|
||||
next-line: "{prefix}&#D4AF37&l{world} &7next trigger: &#B9C63F{trigger}&7. Next size: &#D4AF37{next_size}&7."
|
||||
legacy-check: "{prefix}&#D4AF37&l{world} &7legacy scan: &#B9C63F{locations} &7location(s), required size &#D4AF37{required_size}&7, recommended startup size &#B9C63F{recommended_size}&7. Farthest: &#A8873F{farthest}&7."
|
||||
legacy-scan: "{prefix}&#B9C63FScanned &#D4AF37{scanned} &#B9C63Fplayer(s). Matched &#D4AF37{matched} &#B9C63Fat &#D4AF37{minimum}&#B9C63F+. Added &#D4AF37{added}&#B9C63F. Legacy total: &#D4AF37{total}&7."
|
||||
legacy-list-header: "{prefix}&#B9C63FLegacy users: &#D4AF37{count}&7. Showing up to 20."
|
||||
legacy-list-line: "&8- &#D4AF37{name} &7playtime: &#B9C63F{playtime} &7source: &#A8873F{source}"
|
||||
legacy-add: "{prefix}&#D4AF37{name} &7was &#B9C63F{status} &7as a legacy user."
|
||||
legacy-remove: "{prefix}&#D4AF37{name} &7legacy status: &#B9C63F{status}&7."
|
||||
tp-border: "{prefix}&#B9C63FTeleported to the &#D4AF37{side} &#B9C63Fborder wall for &#D4AF37{world} &8(&7size {size}&8)&7."
|
||||
tp-border-failed: "{prefix}&cCould not teleport to &#D4AF37{world}&c border: &7{reason}"
|
||||
expanded: "{prefix}&#B9C63FExpanded &#D4AF37&l{world} &#B9C63Ffrom &#A8873F{old_size} &#B9C63Fto &#D4AF37{new_size}&7."
|
||||
expand-failed: "{prefix}&cCould not expand &#D4AF37{world}&c: &7{reason}"
|
||||
set-size: "{prefix}&#B9C63FSet &#D4AF37&l{world} &#B9C63Fborder size to &#D4AF37{new_size}&7."
|
||||
set-size-failed: "{prefix}&cCould not set &#D4AF37{world}&c: &7{reason}"
|
||||
paused: "{prefix}&#D4AF37&l{world} &7automatic progression is now paused."
|
||||
resumed: "{prefix}&#B9C63F{world} automatic progression resumed."
|
||||
reset: "{prefix}&#B9C63FReset progression for &#D4AF37&l{world}&7."
|
||||
gui-disabled: "{prefix}&cThe admin GUI is disabled in config."
|
||||
|
||||
expansion:
|
||||
default-message: "{prefix}&#B9C63F{world} expanded from &#A8873F{old_size} &#B9C63Fto &#D4AF37&l{new_size} &#B9C63Fblocks!"
|
||||
default-title: "A2416&lDirtbagMC"
|
||||
default-subtitle: "&#B9C63F{world} &8| &#D4AF37{old_size} &7-> &#D4AF37{new_size}"
|
||||
default-action-bar: "&#B9C63F{world} &7is now &#D4AF37&l{new_size} &#B9C63Fblocks wide"
|
||||
max-reached: "The world is already at its configured maximum or final phase."
|
||||
paused: "Automatic progression is paused."
|
||||
manual-only: "This world is configured for manual-only progression."
|
||||
missing-world: "The Bukkit world is not loaded or does not exist."
|
||||
dry-run: "Dry-run mode is enabled; no border was changed."
|
||||
no-shrink: "No-shrink protection blocked a smaller size. Add 'force' to the command to override."
|
||||
|
||||
reminders:
|
||||
default-message: "{prefix}&#D4AF37&l{world} &7expands in &#B9C63F{time_left}&7."
|
||||
default-action-bar: "&#D4AF37&l{world} &7expands in &#B9C63F{time_left}"
|
||||
|
||||
gui:
|
||||
world-name: "&#D4AF37&l{key}"
|
||||
world-lore:
|
||||
- "&7World: &#D4AF37{world}"
|
||||
- "&7Enabled: &#B9C63F{enabled}"
|
||||
- "&7Size: &#B9C63F{size}"
|
||||
- "&7Next: &#D4AF37{next_size}"
|
||||
- "&7Mode: &#A8873F{border_mode}"
|
||||
- "&7Trigger: B6B35{trigger}"
|
||||
- "&7Phase: &#A8873F{phase}"
|
||||
- "&7Paused: &c{paused}"
|
||||
- ""
|
||||
- "&#D4AF37Left-click &8- &7expand now"
|
||||
- "&#D4AF37Right-click &8- &7pause/resume"
|
||||
refresh: "&#B9C63FRefresh"
|
||||
close: "&cClose"
|
||||
@@ -0,0 +1,65 @@
|
||||
name: DirtSMP
|
||||
version: 1.0.0
|
||||
main: com.dirtsmp.dirtsmp.DirtSMPPlugin
|
||||
api-version: '1.21'
|
||||
authors:
|
||||
- DirtbagMC
|
||||
description: DirtbagMC-themed configurable world-border progression for Paper SMP servers.
|
||||
softdepend:
|
||||
- PlaceholderAPI
|
||||
commands:
|
||||
dirtsmp:
|
||||
description: Manage DirtbagMC world progression.
|
||||
usage: /dirtsmp
|
||||
aliases:
|
||||
- dsmp
|
||||
- dirtborder
|
||||
permissions:
|
||||
dirtsmp.admin:
|
||||
description: Full access to every DirtbagMC progression command.
|
||||
default: op
|
||||
children:
|
||||
dirtsmp.status: true
|
||||
dirtsmp.expand: true
|
||||
dirtsmp.setsize: true
|
||||
dirtsmp.pause: true
|
||||
dirtsmp.resume: true
|
||||
dirtsmp.reload: true
|
||||
dirtsmp.gui: true
|
||||
dirtsmp.reset: true
|
||||
dirtsmp.legacy: true
|
||||
dirtsmp.tpborder: true
|
||||
dirtsmp.bypass.softborder: true
|
||||
dirtsmp.status:
|
||||
description: View DirtbagMC progression status and legacy access scans.
|
||||
default: true
|
||||
dirtsmp.expand:
|
||||
description: Manually expand a configured world border.
|
||||
default: op
|
||||
dirtsmp.setsize:
|
||||
description: Set a configured world's border size.
|
||||
default: op
|
||||
dirtsmp.pause:
|
||||
description: Pause automatic progression for a world.
|
||||
default: op
|
||||
dirtsmp.resume:
|
||||
description: Resume automatic progression for a world.
|
||||
default: op
|
||||
dirtsmp.reload:
|
||||
description: Reload DirtbagMC progression configuration.
|
||||
default: op
|
||||
dirtsmp.gui:
|
||||
description: Open the DirtbagMC admin GUI.
|
||||
default: op
|
||||
dirtsmp.reset:
|
||||
description: Reset a world's DirtbagMC progression state.
|
||||
default: op
|
||||
dirtsmp.legacy:
|
||||
description: Scan, list, add, and remove DirtbagMC legacy users.
|
||||
default: op
|
||||
dirtsmp.tpborder:
|
||||
description: Teleport near a configured world border for visual inspection.
|
||||
default: op
|
||||
dirtsmp.bypass.softborder:
|
||||
description: Bypass DirtbagMC soft border bounce and correction.
|
||||
default: op
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,414 @@
|
||||
# DirtbagMC progression configuration
|
||||
#
|
||||
# Size terminology:
|
||||
# Every border size in this file is a FULL DIAMETER / WIDTH in blocks, matching Bukkit/Paper's WorldBorder size.
|
||||
# Example: starting-size: 50000 creates a border that is 50,000 blocks wide, roughly 25,000 blocks from center to edge.
|
||||
#
|
||||
# Growth modes:
|
||||
# INCREMENTAL - each expansion adds growth-amount to the current full diameter.
|
||||
# PHASE - each expansion moves to the next exact size listed under phases.
|
||||
#
|
||||
# Border modes:
|
||||
# WORLD_BORDER - use Minecraft/Paper's real WorldBorder.
|
||||
# SOFT_BORDER - do not enforce the vanilla border; bounce players off an invisible DirtbagMC border with effects.
|
||||
#
|
||||
# Trigger modes:
|
||||
# MANUAL, TIME, UNIQUE_PLAYERS, ONLINE_PLAYERS, ACTIVE_PLAYERS,
|
||||
# TIME_OR_UNIQUE_PLAYERS, TIME_AND_UNIQUE_PLAYERS,
|
||||
# TIME_OR_ONLINE_PLAYERS, TIME_AND_ONLINE_PLAYERS,
|
||||
# TIME_OR_ACTIVE_PLAYERS, TIME_AND_ACTIVE_PLAYERS.
|
||||
|
||||
settings:
|
||||
dry-run: false
|
||||
debug: false
|
||||
poll-interval-seconds: 30
|
||||
apply-borders-on-startup: true
|
||||
max-catchup-expansions-per-world: 20
|
||||
|
||||
state:
|
||||
file-name: state.yml
|
||||
save-interval-seconds: 300
|
||||
backup-on-save: true
|
||||
backup-keep: 10
|
||||
|
||||
legacy-users:
|
||||
# Admin scan command:
|
||||
# /dirtsmp legacy scan
|
||||
# Also supported:
|
||||
# /dirtsmp add legacy users scan
|
||||
# Players with at least this much Minecraft playtime are added to state.yml under legacy-users.
|
||||
minimum-playtime: 1h
|
||||
|
||||
history:
|
||||
enabled: true
|
||||
file-name: history.log
|
||||
|
||||
gui:
|
||||
enabled: true
|
||||
title: "A2416&lᴅA2D1B&lɪA351F&lʀA3F24&lᴛA4A2A&lʙB6B35&lᴀ&#A8873F&lɢ&#D4AF37&lᴍ&#B9C63F&lᴄ &8| &#D4AF37&lBorders"
|
||||
|
||||
webhook:
|
||||
enabled: false
|
||||
url: ""
|
||||
timeout-seconds: 8
|
||||
content: "**DirtbagMC progression:** `{world}` expanded from `{old_size}` to `{new_size}` blocks. Reason: `{reason}`"
|
||||
|
||||
command-hooks:
|
||||
enabled: true
|
||||
before-expansion:
|
||||
# - "say Preparing expansion for {world} to {new_size}"
|
||||
after-expansion:
|
||||
# Useful for tools like Chunky:
|
||||
# - "chunky world {world}"
|
||||
# - "chunky border"
|
||||
# - "chunky start"
|
||||
|
||||
milestone-rewards:
|
||||
enabled: true
|
||||
# Commands run from console after an expansion reaches a configured milestone.
|
||||
# Placeholders: {world}, {key}, {old_size}, {new_size}, {max_size}, {reason}, {actor}, {phase}, {phase_name}, {expansion_count}
|
||||
every-expansion: []
|
||||
by-expansion-count:
|
||||
# Example rewards:
|
||||
# 1:
|
||||
# - "broadcast DirtbagMC milestone 1 reached in {world}!"
|
||||
# - "crate key giveall beta 1"
|
||||
1: []
|
||||
3: []
|
||||
5: []
|
||||
by-phase-index:
|
||||
1: []
|
||||
by-phase-name:
|
||||
Frontier Era: []
|
||||
First Ring: []
|
||||
|
||||
worlds:
|
||||
overworld:
|
||||
enabled: true
|
||||
world: world
|
||||
center-x: 0.0
|
||||
center-z: 0.0
|
||||
starting-size: 50000
|
||||
border-mode: SOFT_BORDER
|
||||
growth-mode: INCREMENTAL
|
||||
growth-amount: 10000
|
||||
max-size: 200000
|
||||
transition-seconds: 300
|
||||
manual-only: false
|
||||
import-current-border: false
|
||||
no-shrink-protection: true
|
||||
enforce-max-size: true
|
||||
command-hooks:
|
||||
before-expansion: []
|
||||
after-expansion: []
|
||||
milestone-rewards:
|
||||
enabled: true
|
||||
every-expansion: []
|
||||
by-expansion-count:
|
||||
1: []
|
||||
2: []
|
||||
3: []
|
||||
by-phase-index: {}
|
||||
by-phase-name: {}
|
||||
legacy-access:
|
||||
enabled: true
|
||||
# This protects beta builds from being stranded when progression starts after launch.
|
||||
# The plugin expands the initial/saved border enough to include known locations plus padding.
|
||||
include-current-border: false
|
||||
include-online-players: true
|
||||
include-offline-last-locations: true
|
||||
include-respawn-locations: true
|
||||
# When true, player last/respawn locations only count if the player is in the scanned legacy-users list.
|
||||
player-locations-require-legacy-user: true
|
||||
reconcile-on-startup: true
|
||||
allow-start-above-max-size: true
|
||||
padding: 1024
|
||||
# Add homes, towns, bases, or Essentials homes that cannot be discovered from Paper playerdata.
|
||||
# Border math uses full diameter/width, centered on center-x/center-z.
|
||||
locations: []
|
||||
# Example:
|
||||
# locations:
|
||||
# - name: "Beta town"
|
||||
# x: 31500
|
||||
# z: -22400
|
||||
soft-border:
|
||||
# Premium invisible border mode. Used only when border-mode is SOFT_BORDER.
|
||||
# release-vanilla-border removes any old vanilla border enforcement by setting it to vanilla-border-size.
|
||||
release-vanilla-border: true
|
||||
vanilla-border-size: 59999968
|
||||
ignore-creative: false
|
||||
ignore-spectator: true
|
||||
bypass-permission: dirtsmp.bypass.softborder
|
||||
inside-buffer: 1.5
|
||||
bounce-strength: 1.85
|
||||
vertical-boost: 0.35
|
||||
protect-mounted-entities: true
|
||||
# Gentler values keep horses, camels, boats, and minecarts from taking weird launch angles.
|
||||
mounted-bounce-strength: 1.15
|
||||
mounted-vertical-boost: 0.12
|
||||
max-outside-distance-before-teleport: 24
|
||||
cooldown: 900ms
|
||||
message: "{prefix}&#D4AF37&lThe DirtbagMC border &7throws you back."
|
||||
action-bar: "&#D4AF37&lBorder reached &8| &7turn back"
|
||||
sound: ENTITY_SLIME_JUMP
|
||||
volume: 0.65
|
||||
pitch: 0.65
|
||||
particles:
|
||||
enabled: true
|
||||
# DUST uses the DirtbagMC gold color. Other Bukkit particle names also work.
|
||||
particle: DUST
|
||||
count: 3
|
||||
offset: 0.05
|
||||
speed: 0.0
|
||||
interval-ticks: 10
|
||||
view-distance: 96
|
||||
spacing: 3
|
||||
broadcasts:
|
||||
enabled: true
|
||||
world-only: false
|
||||
message: "{prefix}&#B9C63FThe dirt expands. &#D4AF37&l{world} &#B9C63Fhas grown from &#A8873F{old_size} &#B9C63Fto &#D4AF37&l{new_size} &#B9C63Fblocks."
|
||||
title: "A2416&lᴅA2D1B&lɪA351F&lʀA3F24&lᴛA4A2A&lʙB6B35&lᴀ&#A8873F&lɢ&#D4AF37&lᴍ&#B9C63F&lᴄ"
|
||||
subtitle: "&#B9C63F{world} &8| &#D4AF37{old_size} &7-> &#D4AF37{new_size}"
|
||||
action-bar: "&#D4AF37&l{world} &7border is now &#B9C63F{new_size} &7blocks wide"
|
||||
sound: ENTITY_ENDER_DRAGON_GROWL
|
||||
volume: 0.7
|
||||
pitch: 1.25
|
||||
reminders:
|
||||
enabled: true
|
||||
before:
|
||||
- 1d
|
||||
- 12h
|
||||
- 1h
|
||||
- 10m
|
||||
message: "{prefix}&#D4AF37&l{world} &7expands in &#B9C63F{time_left}&7."
|
||||
action-bar: "&#D4AF37&l{world} &7expands in &#B9C63F{time_left}"
|
||||
sound: BLOCK_NOTE_BLOCK_PLING
|
||||
volume: 0.45
|
||||
pitch: 1.4
|
||||
triggers:
|
||||
mode: TIME_OR_UNIQUE_PLAYERS
|
||||
time-interval: 7d
|
||||
unique-players-every: 50
|
||||
online-players-threshold: 35
|
||||
active-players-threshold: 120
|
||||
active-window: 7d
|
||||
exclude-vanished: true
|
||||
player-trigger-cooldown: 12h
|
||||
catch-up: ONE
|
||||
phases:
|
||||
- name: Spawn Era
|
||||
size: 50000
|
||||
message: ""
|
||||
- name: Frontier Era
|
||||
size: 75000
|
||||
message: ""
|
||||
|
||||
end:
|
||||
enabled: true
|
||||
world: world_the_end
|
||||
center-x: 0.0
|
||||
center-z: 0.0
|
||||
starting-size: 2000
|
||||
border-mode: SOFT_BORDER
|
||||
growth-mode: PHASE
|
||||
growth-amount: 2000
|
||||
max-size: 20000
|
||||
transition-seconds: 180
|
||||
manual-only: false
|
||||
import-current-border: false
|
||||
no-shrink-protection: true
|
||||
enforce-max-size: true
|
||||
command-hooks:
|
||||
before-expansion: []
|
||||
after-expansion: []
|
||||
milestone-rewards:
|
||||
enabled: true
|
||||
every-expansion: []
|
||||
by-expansion-count:
|
||||
1: []
|
||||
2: []
|
||||
by-phase-index:
|
||||
1: []
|
||||
2: []
|
||||
by-phase-name:
|
||||
First Ring: []
|
||||
Chorus Frontier: []
|
||||
legacy-access:
|
||||
enabled: true
|
||||
include-current-border: false
|
||||
include-online-players: true
|
||||
include-offline-last-locations: true
|
||||
include-respawn-locations: true
|
||||
player-locations-require-legacy-user: true
|
||||
reconcile-on-startup: true
|
||||
allow-start-above-max-size: true
|
||||
padding: 512
|
||||
locations: []
|
||||
soft-border:
|
||||
release-vanilla-border: true
|
||||
vanilla-border-size: 59999968
|
||||
ignore-creative: false
|
||||
ignore-spectator: true
|
||||
bypass-permission: dirtsmp.bypass.softborder
|
||||
inside-buffer: 1.5
|
||||
bounce-strength: 2.05
|
||||
vertical-boost: 0.45
|
||||
protect-mounted-entities: true
|
||||
mounted-bounce-strength: 1.2
|
||||
mounted-vertical-boost: 0.12
|
||||
max-outside-distance-before-teleport: 18
|
||||
cooldown: 900ms
|
||||
message: "{prefix}A2416&lThe End border &7snaps you back."
|
||||
action-bar: "A2416&lThe End border &8| &7turn back"
|
||||
sound: ENTITY_ENDERMAN_TELEPORT
|
||||
volume: 0.55
|
||||
pitch: 0.8
|
||||
particles:
|
||||
enabled: true
|
||||
particle: DUST
|
||||
count: 3
|
||||
offset: 0.05
|
||||
speed: 0.0
|
||||
interval-ticks: 10
|
||||
view-distance: 72
|
||||
spacing: 2.5
|
||||
broadcasts:
|
||||
enabled: true
|
||||
world-only: false
|
||||
message: "{prefix}A2416&lThe End &#B9C63Fhas entered &#D4AF37&l{phase_name}&#B9C63F: &#D4AF37{new_size} &#B9C63Fblocks wide."
|
||||
title: "A2416&lThe End Opens"
|
||||
subtitle: "&#D4AF37{phase_name} &8| &#B9C63F{new_size} blocks"
|
||||
action-bar: "A2416&lThe End &7border is now &#D4AF37{new_size}"
|
||||
sound: ENTITY_ENDER_DRAGON_DEATH
|
||||
volume: 0.8
|
||||
pitch: 1.0
|
||||
reminders:
|
||||
enabled: true
|
||||
before:
|
||||
- 12h
|
||||
- 1h
|
||||
- 10m
|
||||
message: "{prefix}A2416&lThe End &7expands in &#D4AF37{time_left}&7."
|
||||
action-bar: "A2416&lThe End &7expands in &#D4AF37{time_left}"
|
||||
sound: BLOCK_NOTE_BLOCK_PLING
|
||||
volume: 0.45
|
||||
pitch: 1.6
|
||||
triggers:
|
||||
mode: TIME_AND_UNIQUE_PLAYERS
|
||||
time-interval: 10d
|
||||
unique-players-every: 100
|
||||
online-players-threshold: 40
|
||||
active-players-threshold: 150
|
||||
active-window: 7d
|
||||
exclude-vanished: true
|
||||
player-trigger-cooldown: 12h
|
||||
catch-up: ONE
|
||||
phases:
|
||||
- name: Outer Silence
|
||||
size: 2000
|
||||
message: ""
|
||||
- name: First Ring
|
||||
size: 4000
|
||||
message: "{prefix}A2416&lThe End &#B9C63Fstirs. &#D4AF37The first outer ring &#B9C63Fis now reachable."
|
||||
- name: Chorus Frontier
|
||||
size: 6000
|
||||
message: "{prefix}A2416&lThe End &#B9C63Fgrows again. &#D4AF37New islands await."
|
||||
- name: Dragon's Wake
|
||||
size: 8000
|
||||
message: "{prefix}A2416&lThe End &#B9C63Fborder has expanded to &#D4AF37{new_size} &#B9C63Fblocks."
|
||||
|
||||
nether:
|
||||
enabled: false
|
||||
world: world_nether
|
||||
center-x: 0.0
|
||||
center-z: 0.0
|
||||
starting-size: 10000
|
||||
border-mode: SOFT_BORDER
|
||||
growth-mode: INCREMENTAL
|
||||
growth-amount: 2500
|
||||
max-size: 50000
|
||||
transition-seconds: 120
|
||||
manual-only: false
|
||||
import-current-border: false
|
||||
no-shrink-protection: true
|
||||
enforce-max-size: true
|
||||
command-hooks:
|
||||
before-expansion: []
|
||||
after-expansion: []
|
||||
milestone-rewards:
|
||||
enabled: true
|
||||
every-expansion: []
|
||||
by-expansion-count:
|
||||
1: []
|
||||
by-phase-index: {}
|
||||
by-phase-name: {}
|
||||
legacy-access:
|
||||
enabled: false
|
||||
include-current-border: false
|
||||
include-online-players: true
|
||||
include-offline-last-locations: true
|
||||
include-respawn-locations: true
|
||||
player-locations-require-legacy-user: true
|
||||
reconcile-on-startup: true
|
||||
allow-start-above-max-size: true
|
||||
padding: 512
|
||||
locations: []
|
||||
soft-border:
|
||||
release-vanilla-border: true
|
||||
vanilla-border-size: 59999968
|
||||
ignore-creative: false
|
||||
ignore-spectator: true
|
||||
bypass-permission: dirtsmp.bypass.softborder
|
||||
inside-buffer: 1.5
|
||||
bounce-strength: 1.9
|
||||
vertical-boost: 0.35
|
||||
protect-mounted-entities: true
|
||||
mounted-bounce-strength: 1.15
|
||||
mounted-vertical-boost: 0.12
|
||||
max-outside-distance-before-teleport: 18
|
||||
cooldown: 900ms
|
||||
message: "{prefix}A4A2A&lThe Nether border &7throws you back."
|
||||
action-bar: "A4A2A&lNether border &8| &7turn back"
|
||||
sound: ENTITY_BLAZE_HURT
|
||||
volume: 0.55
|
||||
pitch: 0.8
|
||||
particles:
|
||||
enabled: true
|
||||
particle: DUST
|
||||
count: 3
|
||||
offset: 0.05
|
||||
speed: 0.0
|
||||
interval-ticks: 10
|
||||
view-distance: 72
|
||||
spacing: 2.5
|
||||
broadcasts:
|
||||
enabled: true
|
||||
world-only: false
|
||||
message: "{prefix}A4A2A&lThe Nether &#B9C63Fhas expanded to &#D4AF37{new_size} &#B9C63Fblocks."
|
||||
title: "A4A2A&lNether Expanded"
|
||||
subtitle: "&#A8873F{old_size} &7-> &#D4AF37{new_size}"
|
||||
action-bar: "A4A2A&lThe Nether &7border is now &#D4AF37{new_size}"
|
||||
sound: ENTITY_WITHER_SPAWN
|
||||
volume: 0.65
|
||||
pitch: 1.1
|
||||
reminders:
|
||||
enabled: true
|
||||
before:
|
||||
- 1h
|
||||
- 10m
|
||||
message: "{prefix}A4A2A&lThe Nether &7expands in &#D4AF37{time_left}&7."
|
||||
action-bar: "A4A2A&lThe Nether &7expands in &#D4AF37{time_left}"
|
||||
sound: BLOCK_NOTE_BLOCK_PLING
|
||||
volume: 0.45
|
||||
pitch: 1.2
|
||||
triggers:
|
||||
mode: MANUAL
|
||||
time-interval: 7d
|
||||
unique-players-every: 50
|
||||
online-players-threshold: 30
|
||||
active-players-threshold: 100
|
||||
active-window: 7d
|
||||
exclude-vanished: true
|
||||
player-trigger-cooldown: 12h
|
||||
catch-up: NONE
|
||||
phases: []
|
||||
@@ -0,0 +1,76 @@
|
||||
prefix: "A2416&lᴅA2D1B&lɪA351F&lʀA3F24&lᴛA4A2A&lʙB6B35&lᴀ&#A8873F&lɢ&#D4AF37&lᴍ&#B9C63F&lᴄ &8| &7"
|
||||
|
||||
commands:
|
||||
no-permission: "{prefix}&cYou do not have permission to do that."
|
||||
player-only: "{prefix}&cOnly players can use that command."
|
||||
unknown-world: "{prefix}&cUnknown configured world: &#D4AF37{world}"
|
||||
invalid-number: "{prefix}&cThat size must be a positive number."
|
||||
reload: "{prefix}&#B9C63FConfiguration, messages, and DirtbagMC progression rules reloaded."
|
||||
saved: "{prefix}&#B9C63FState saved."
|
||||
help:
|
||||
- "A2416&lᴅA2D1B&lɪA351F&lʀA3F24&lᴛA4A2A&lʙB6B35&lᴀ&#A8873F&lɢ&#D4AF37&lᴍ&#B9C63F&lᴄ &8| &#D4AF37&lBorder Progression"
|
||||
- "B6B35/dirtsmp status [world] &8- &7view progression"
|
||||
- "B6B35/dirtsmp next [world] &8- &7view upcoming triggers"
|
||||
- "B6B35/dirtsmp legacycheck <world> &8- &7scan beta home safety"
|
||||
- "B6B35/dirtsmp tpborder <world> [side] &8- &7teleport near a border wall"
|
||||
- "B6B35/dirtsmp legacy scan [1h] &8- &7mark legacy players by playtime"
|
||||
- "B6B35/dirtsmp legacy list &8- &7show marked legacy players"
|
||||
- "B6B35/dirtsmp expand <world> &8- &7expand now"
|
||||
- "B6B35/dirtsmp setsize <world> <size> [force] &8- &7set border size"
|
||||
- "B6B35/dirtsmp pause <world> &8- &7pause automatic progression"
|
||||
- "B6B35/dirtsmp resume <world> &8- &7resume automatic progression"
|
||||
- "B6B35/dirtsmp reset <world> [force] &8- &7reset progression"
|
||||
- "B6B35/dirtsmp gui &8- &7open admin panel"
|
||||
- "B6B35/dirtsmp reload &8- &7reload config"
|
||||
status-header: "{prefix}&#B9C63FTracking &#D4AF37&l{count} &#B9C63Fconfigured world(s)."
|
||||
status-line: "&8- &#D4AF37&l{key} &8(&7{world}&8) &7enabled: &#B9C63F{enabled} &7size: &#B9C63F{size} &7next: &#D4AF37{next_size} &7mode: &#A8873F{border_mode} &7phase: &#A8873F{phase} &7paused: &c{paused}"
|
||||
next-line: "{prefix}&#D4AF37&l{world} &7next trigger: &#B9C63F{trigger}&7. Next size: &#D4AF37{next_size}&7."
|
||||
legacy-check: "{prefix}&#D4AF37&l{world} &7legacy scan: &#B9C63F{locations} &7location(s), required size &#D4AF37{required_size}&7, recommended startup size &#B9C63F{recommended_size}&7. Farthest: &#A8873F{farthest}&7."
|
||||
legacy-scan: "{prefix}&#B9C63FScanned &#D4AF37{scanned} &#B9C63Fplayer(s). Matched &#D4AF37{matched} &#B9C63Fat &#D4AF37{minimum}&#B9C63F+. Added &#D4AF37{added}&#B9C63F. Legacy total: &#D4AF37{total}&7."
|
||||
legacy-list-header: "{prefix}&#B9C63FLegacy users: &#D4AF37{count}&7. Showing up to 20."
|
||||
legacy-list-line: "&8- &#D4AF37{name} &7playtime: &#B9C63F{playtime} &7source: &#A8873F{source}"
|
||||
legacy-add: "{prefix}&#D4AF37{name} &7was &#B9C63F{status} &7as a legacy user."
|
||||
legacy-remove: "{prefix}&#D4AF37{name} &7legacy status: &#B9C63F{status}&7."
|
||||
tp-border: "{prefix}&#B9C63FTeleported to the &#D4AF37{side} &#B9C63Fborder wall for &#D4AF37{world} &8(&7size {size}&8)&7."
|
||||
tp-border-failed: "{prefix}&cCould not teleport to &#D4AF37{world}&c border: &7{reason}"
|
||||
expanded: "{prefix}&#B9C63FExpanded &#D4AF37&l{world} &#B9C63Ffrom &#A8873F{old_size} &#B9C63Fto &#D4AF37{new_size}&7."
|
||||
expand-failed: "{prefix}&cCould not expand &#D4AF37{world}&c: &7{reason}"
|
||||
set-size: "{prefix}&#B9C63FSet &#D4AF37&l{world} &#B9C63Fborder size to &#D4AF37{new_size}&7."
|
||||
set-size-failed: "{prefix}&cCould not set &#D4AF37{world}&c: &7{reason}"
|
||||
paused: "{prefix}&#D4AF37&l{world} &7automatic progression is now paused."
|
||||
resumed: "{prefix}&#B9C63F{world} automatic progression resumed."
|
||||
reset: "{prefix}&#B9C63FReset progression for &#D4AF37&l{world}&7."
|
||||
gui-disabled: "{prefix}&cThe admin GUI is disabled in config."
|
||||
|
||||
expansion:
|
||||
default-message: "{prefix}&#B9C63F{world} expanded from &#A8873F{old_size} &#B9C63Fto &#D4AF37&l{new_size} &#B9C63Fblocks!"
|
||||
default-title: "A2416&lDirtbagMC"
|
||||
default-subtitle: "&#B9C63F{world} &8| &#D4AF37{old_size} &7-> &#D4AF37{new_size}"
|
||||
default-action-bar: "&#B9C63F{world} &7is now &#D4AF37&l{new_size} &#B9C63Fblocks wide"
|
||||
max-reached: "The world is already at its configured maximum or final phase."
|
||||
paused: "Automatic progression is paused."
|
||||
manual-only: "This world is configured for manual-only progression."
|
||||
missing-world: "The Bukkit world is not loaded or does not exist."
|
||||
dry-run: "Dry-run mode is enabled; no border was changed."
|
||||
no-shrink: "No-shrink protection blocked a smaller size. Add 'force' to the command to override."
|
||||
|
||||
reminders:
|
||||
default-message: "{prefix}&#D4AF37&l{world} &7expands in &#B9C63F{time_left}&7."
|
||||
default-action-bar: "&#D4AF37&l{world} &7expands in &#B9C63F{time_left}"
|
||||
|
||||
gui:
|
||||
world-name: "&#D4AF37&l{key}"
|
||||
world-lore:
|
||||
- "&7World: &#D4AF37{world}"
|
||||
- "&7Enabled: &#B9C63F{enabled}"
|
||||
- "&7Size: &#B9C63F{size}"
|
||||
- "&7Next: &#D4AF37{next_size}"
|
||||
- "&7Mode: &#A8873F{border_mode}"
|
||||
- "&7Trigger: B6B35{trigger}"
|
||||
- "&7Phase: &#A8873F{phase}"
|
||||
- "&7Paused: &c{paused}"
|
||||
- ""
|
||||
- "&#D4AF37Left-click &8- &7expand now"
|
||||
- "&#D4AF37Right-click &8- &7pause/resume"
|
||||
refresh: "&#B9C63FRefresh"
|
||||
close: "&cClose"
|
||||
@@ -0,0 +1,65 @@
|
||||
name: DirtSMP
|
||||
version: 1.0.0
|
||||
main: com.dirtsmp.dirtsmp.DirtSMPPlugin
|
||||
api-version: '1.21'
|
||||
authors:
|
||||
- DirtbagMC
|
||||
description: DirtbagMC-themed configurable world-border progression for Paper SMP servers.
|
||||
softdepend:
|
||||
- PlaceholderAPI
|
||||
commands:
|
||||
dirtsmp:
|
||||
description: Manage DirtbagMC world progression.
|
||||
usage: /dirtsmp
|
||||
aliases:
|
||||
- dsmp
|
||||
- dirtborder
|
||||
permissions:
|
||||
dirtsmp.admin:
|
||||
description: Full access to every DirtbagMC progression command.
|
||||
default: op
|
||||
children:
|
||||
dirtsmp.status: true
|
||||
dirtsmp.expand: true
|
||||
dirtsmp.setsize: true
|
||||
dirtsmp.pause: true
|
||||
dirtsmp.resume: true
|
||||
dirtsmp.reload: true
|
||||
dirtsmp.gui: true
|
||||
dirtsmp.reset: true
|
||||
dirtsmp.legacy: true
|
||||
dirtsmp.tpborder: true
|
||||
dirtsmp.bypass.softborder: true
|
||||
dirtsmp.status:
|
||||
description: View DirtbagMC progression status and legacy access scans.
|
||||
default: true
|
||||
dirtsmp.expand:
|
||||
description: Manually expand a configured world border.
|
||||
default: op
|
||||
dirtsmp.setsize:
|
||||
description: Set a configured world's border size.
|
||||
default: op
|
||||
dirtsmp.pause:
|
||||
description: Pause automatic progression for a world.
|
||||
default: op
|
||||
dirtsmp.resume:
|
||||
description: Resume automatic progression for a world.
|
||||
default: op
|
||||
dirtsmp.reload:
|
||||
description: Reload DirtbagMC progression configuration.
|
||||
default: op
|
||||
dirtsmp.gui:
|
||||
description: Open the DirtbagMC admin GUI.
|
||||
default: op
|
||||
dirtsmp.reset:
|
||||
description: Reset a world's DirtbagMC progression state.
|
||||
default: op
|
||||
dirtsmp.legacy:
|
||||
description: Scan, list, add, and remove DirtbagMC legacy users.
|
||||
default: op
|
||||
dirtsmp.tpborder:
|
||||
description: Teleport near a configured world border for visual inspection.
|
||||
default: op
|
||||
dirtsmp.bypass.softborder:
|
||||
description: Bypass DirtbagMC soft border bounce and correction.
|
||||
default: op
|
||||
@@ -0,0 +1,3 @@
|
||||
artifactId=DirtSMP
|
||||
groupId=com.dirtsmp
|
||||
version=1.0.0
|
||||
@@ -0,0 +1,42 @@
|
||||
com/dirtsmp/dirtsmp/service/TimeUtil.class
|
||||
com/dirtsmp/dirtsmp/model/TriggerMode.class
|
||||
com/dirtsmp/dirtsmp/config/MessageManager.class
|
||||
com/dirtsmp/dirtsmp/model/ExpansionReason.class
|
||||
com/dirtsmp/dirtsmp/service/HistoryLogger.class
|
||||
com/dirtsmp/dirtsmp/service/TriggerEvaluator$1.class
|
||||
com/dirtsmp/dirtsmp/task/ScheduleManager.class
|
||||
com/dirtsmp/dirtsmp/state/PlayerStatsManager.class
|
||||
com/dirtsmp/dirtsmp/config/ConfigManager.class
|
||||
com/dirtsmp/dirtsmp/service/SoftBorderManager.class
|
||||
com/dirtsmp/dirtsmp/model/TriggerSettings.class
|
||||
com/dirtsmp/dirtsmp/service/SoftBorderManager$Bounds.class
|
||||
com/dirtsmp/dirtsmp/model/WorldRule$MilestoneRewardSettings.class
|
||||
com/dirtsmp/dirtsmp/hook/PlaceholderHook.class
|
||||
com/dirtsmp/dirtsmp/model/GrowthMode.class
|
||||
com/dirtsmp/dirtsmp/model/WorldRule$LegacyLocation.class
|
||||
com/dirtsmp/dirtsmp/service/TriggerEvaluator.class
|
||||
com/dirtsmp/dirtsmp/state/StateManager.class
|
||||
com/dirtsmp/dirtsmp/model/WorldRule.class
|
||||
com/dirtsmp/dirtsmp/gui/AdminGuiManager.class
|
||||
com/dirtsmp/dirtsmp/model/WorldRule$LegacyAccessSettings.class
|
||||
com/dirtsmp/dirtsmp/model/WorldRule$AnnouncementSettings.class
|
||||
com/dirtsmp/dirtsmp/state/StateManager$LegacyUserRecord.class
|
||||
com/dirtsmp/dirtsmp/service/WebhookNotifier.class
|
||||
com/dirtsmp/dirtsmp/listener/PlayerListener.class
|
||||
com/dirtsmp/dirtsmp/hook/DirtSMPPlaceholderExpansion.class
|
||||
com/dirtsmp/dirtsmp/command/DirtSMPCommand.class
|
||||
com/dirtsmp/dirtsmp/DirtSMPPlugin.class
|
||||
com/dirtsmp/dirtsmp/model/PhaseDefinition.class
|
||||
com/dirtsmp/dirtsmp/service/BorderManager.class
|
||||
com/dirtsmp/dirtsmp/model/WorldRule$ReminderSettings.class
|
||||
com/dirtsmp/dirtsmp/model/ExpansionResult.class
|
||||
com/dirtsmp/dirtsmp/model/LegacyAccessReport.class
|
||||
com/dirtsmp/dirtsmp/state/PlayerStatsManager$PlayerRecord.class
|
||||
com/dirtsmp/dirtsmp/state/WorldProgress.class
|
||||
com/dirtsmp/dirtsmp/service/CommandHookManager.class
|
||||
com/dirtsmp/dirtsmp/model/BorderMode.class
|
||||
com/dirtsmp/dirtsmp/service/BorderManager$LegacyAccumulator.class
|
||||
com/dirtsmp/dirtsmp/model/WorldRule$SoftBorderSettings.class
|
||||
com/dirtsmp/dirtsmp/model/TriggerDecision.class
|
||||
com/dirtsmp/dirtsmp/model/CatchUpMode.class
|
||||
com/dirtsmp/dirtsmp/gui/AdminGuiManager$GuiHolder.class
|
||||
@@ -0,0 +1,30 @@
|
||||
/home/bitnix/Desktop/DirtSMP/src/main/java/com/dirtsmp/dirtsmp/DirtSMPPlugin.java
|
||||
/home/bitnix/Desktop/DirtSMP/src/main/java/com/dirtsmp/dirtsmp/command/DirtSMPCommand.java
|
||||
/home/bitnix/Desktop/DirtSMP/src/main/java/com/dirtsmp/dirtsmp/config/ConfigManager.java
|
||||
/home/bitnix/Desktop/DirtSMP/src/main/java/com/dirtsmp/dirtsmp/config/MessageManager.java
|
||||
/home/bitnix/Desktop/DirtSMP/src/main/java/com/dirtsmp/dirtsmp/gui/AdminGuiManager.java
|
||||
/home/bitnix/Desktop/DirtSMP/src/main/java/com/dirtsmp/dirtsmp/hook/DirtSMPPlaceholderExpansion.java
|
||||
/home/bitnix/Desktop/DirtSMP/src/main/java/com/dirtsmp/dirtsmp/hook/PlaceholderHook.java
|
||||
/home/bitnix/Desktop/DirtSMP/src/main/java/com/dirtsmp/dirtsmp/listener/PlayerListener.java
|
||||
/home/bitnix/Desktop/DirtSMP/src/main/java/com/dirtsmp/dirtsmp/model/BorderMode.java
|
||||
/home/bitnix/Desktop/DirtSMP/src/main/java/com/dirtsmp/dirtsmp/model/CatchUpMode.java
|
||||
/home/bitnix/Desktop/DirtSMP/src/main/java/com/dirtsmp/dirtsmp/model/ExpansionReason.java
|
||||
/home/bitnix/Desktop/DirtSMP/src/main/java/com/dirtsmp/dirtsmp/model/ExpansionResult.java
|
||||
/home/bitnix/Desktop/DirtSMP/src/main/java/com/dirtsmp/dirtsmp/model/GrowthMode.java
|
||||
/home/bitnix/Desktop/DirtSMP/src/main/java/com/dirtsmp/dirtsmp/model/LegacyAccessReport.java
|
||||
/home/bitnix/Desktop/DirtSMP/src/main/java/com/dirtsmp/dirtsmp/model/PhaseDefinition.java
|
||||
/home/bitnix/Desktop/DirtSMP/src/main/java/com/dirtsmp/dirtsmp/model/TriggerDecision.java
|
||||
/home/bitnix/Desktop/DirtSMP/src/main/java/com/dirtsmp/dirtsmp/model/TriggerMode.java
|
||||
/home/bitnix/Desktop/DirtSMP/src/main/java/com/dirtsmp/dirtsmp/model/TriggerSettings.java
|
||||
/home/bitnix/Desktop/DirtSMP/src/main/java/com/dirtsmp/dirtsmp/model/WorldRule.java
|
||||
/home/bitnix/Desktop/DirtSMP/src/main/java/com/dirtsmp/dirtsmp/service/BorderManager.java
|
||||
/home/bitnix/Desktop/DirtSMP/src/main/java/com/dirtsmp/dirtsmp/service/CommandHookManager.java
|
||||
/home/bitnix/Desktop/DirtSMP/src/main/java/com/dirtsmp/dirtsmp/service/HistoryLogger.java
|
||||
/home/bitnix/Desktop/DirtSMP/src/main/java/com/dirtsmp/dirtsmp/service/SoftBorderManager.java
|
||||
/home/bitnix/Desktop/DirtSMP/src/main/java/com/dirtsmp/dirtsmp/service/TimeUtil.java
|
||||
/home/bitnix/Desktop/DirtSMP/src/main/java/com/dirtsmp/dirtsmp/service/TriggerEvaluator.java
|
||||
/home/bitnix/Desktop/DirtSMP/src/main/java/com/dirtsmp/dirtsmp/service/WebhookNotifier.java
|
||||
/home/bitnix/Desktop/DirtSMP/src/main/java/com/dirtsmp/dirtsmp/state/PlayerStatsManager.java
|
||||
/home/bitnix/Desktop/DirtSMP/src/main/java/com/dirtsmp/dirtsmp/state/StateManager.java
|
||||
/home/bitnix/Desktop/DirtSMP/src/main/java/com/dirtsmp/dirtsmp/state/WorldProgress.java
|
||||
/home/bitnix/Desktop/DirtSMP/src/main/java/com/dirtsmp/dirtsmp/task/ScheduleManager.java
|
||||
Reference in New Issue
Block a user