first
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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.dirtbagmc</groupId>
|
||||
<artifactId>DirtBounties</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>DirtBounties</name>
|
||||
<description>Premium Paper bounty system for DirtbagMC.</description>
|
||||
<url>https://dirtbagmc.com</url>
|
||||
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<paper.version>1.21.8-R0.1-SNAPSHOT</paper.version>
|
||||
<placeholderapi.version>2.11.6</placeholderapi.version>
|
||||
</properties>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>papermc</id>
|
||||
<url>https://repo.papermc.io/repository/maven-public/</url>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>placeholderapi</id>
|
||||
<url>https://repo.extendedclip.com/content/repositories/placeholderapi/</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>DirtBounties-${project.version}</finalName>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<filtering>false</filtering>
|
||||
</resource>
|
||||
</resources>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.13.0</version>
|
||||
<configuration>
|
||||
<release>${java.version}</release>
|
||||
<encoding>${project.build.sourceEncoding}</encoding>
|
||||
</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,204 @@
|
||||
package com.dirtbagmc.dirtbounties;
|
||||
|
||||
import com.dirtbagmc.dirtbounties.command.BountyAdminCommand;
|
||||
import com.dirtbagmc.dirtbounties.command.BountyCommand;
|
||||
import com.dirtbagmc.dirtbounties.config.ConfigService;
|
||||
import com.dirtbagmc.dirtbounties.economy.EconomyService;
|
||||
import com.dirtbagmc.dirtbounties.gui.GuiManager;
|
||||
import com.dirtbagmc.dirtbounties.hook.DirtBountiesExpansion;
|
||||
import com.dirtbagmc.dirtbounties.listener.BountyListener;
|
||||
import com.dirtbagmc.dirtbounties.service.AntiAbuseService;
|
||||
import com.dirtbagmc.dirtbounties.service.BountyService;
|
||||
import com.dirtbagmc.dirtbounties.service.CombatTracker;
|
||||
import com.dirtbagmc.dirtbounties.service.HistoryService;
|
||||
import com.dirtbagmc.dirtbounties.service.MessageService;
|
||||
import com.dirtbagmc.dirtbounties.service.PlayerCacheService;
|
||||
import com.dirtbagmc.dirtbounties.storage.StorageManager;
|
||||
import com.dirtbagmc.dirtbounties.util.TimeUtil;
|
||||
import com.dirtbagmc.dirtbounties.webhook.WebhookService;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.command.PluginCommand;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public final class DirtBountiesPlugin extends JavaPlugin {
|
||||
private ConfigService configService;
|
||||
private MessageService messageService;
|
||||
private StorageManager storageManager;
|
||||
private EconomyService economyService;
|
||||
private HistoryService historyService;
|
||||
private PlayerCacheService playerCacheService;
|
||||
private CombatTracker combatTracker;
|
||||
private AntiAbuseService antiAbuseService;
|
||||
private WebhookService webhookService;
|
||||
private BountyService bountyService;
|
||||
private GuiManager guiManager;
|
||||
private BukkitTask cleanupTask;
|
||||
private BukkitTask autosaveTask;
|
||||
private Object placeholderExpansion;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
try {
|
||||
bootstrap();
|
||||
registerCommands();
|
||||
registerEvents();
|
||||
registerPlaceholders();
|
||||
scheduleTasks();
|
||||
getLogger().info("DirtBounties enabled with " + bountyService.activeBounties().size() + " active bounties.");
|
||||
} catch (RuntimeException ex) {
|
||||
getLogger().log(Level.SEVERE, "DirtBounties failed to enable safely.", ex);
|
||||
Bukkit.getPluginManager().disablePlugin(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
Bukkit.getScheduler().cancelTasks(this);
|
||||
if (bountyService != null) {
|
||||
bountyService.save();
|
||||
}
|
||||
if (historyService != null) {
|
||||
historyService.save();
|
||||
}
|
||||
if (playerCacheService != null) {
|
||||
playerCacheService.save();
|
||||
}
|
||||
if (guiManager != null) {
|
||||
guiManager.clear();
|
||||
}
|
||||
if (combatTracker != null) {
|
||||
combatTracker.clear();
|
||||
}
|
||||
if (antiAbuseService != null) {
|
||||
antiAbuseService.clear();
|
||||
}
|
||||
if (bountyService != null) {
|
||||
bountyService.clearRuntimeState();
|
||||
}
|
||||
cleanupTask = null;
|
||||
autosaveTask = null;
|
||||
getLogger().info("DirtBounties disabled cleanly.");
|
||||
}
|
||||
|
||||
public void reloadDirtBounties() {
|
||||
Bukkit.getScheduler().cancelTasks(this);
|
||||
if (bountyService != null) {
|
||||
bountyService.save();
|
||||
}
|
||||
if (historyService != null) {
|
||||
historyService.save();
|
||||
}
|
||||
if (playerCacheService != null) {
|
||||
playerCacheService.save();
|
||||
}
|
||||
|
||||
configService.load();
|
||||
economyService.initialize();
|
||||
webhookService.initialize();
|
||||
historyService.load();
|
||||
playerCacheService.load();
|
||||
bountyService.load();
|
||||
guiManager.clear();
|
||||
combatTracker.clear();
|
||||
antiAbuseService.clear();
|
||||
scheduleTasks();
|
||||
}
|
||||
|
||||
public BountyService bountyService() {
|
||||
return bountyService;
|
||||
}
|
||||
|
||||
public HistoryService historyService() {
|
||||
return historyService;
|
||||
}
|
||||
|
||||
private void bootstrap() {
|
||||
configService = new ConfigService(this);
|
||||
configService.load();
|
||||
messageService = new MessageService(configService);
|
||||
storageManager = new StorageManager(this);
|
||||
storageManager.initialize();
|
||||
economyService = new EconomyService(this, configService);
|
||||
economyService.initialize();
|
||||
if (!economyService.isReady() && configService.main().getBoolean("economy.fail-if-missing", false)) {
|
||||
throw new IllegalStateException("Configured to fail when no Vault-compatible economy is available.");
|
||||
}
|
||||
|
||||
historyService = new HistoryService(configService, storageManager);
|
||||
historyService.load();
|
||||
playerCacheService = new PlayerCacheService(configService, storageManager);
|
||||
playerCacheService.load();
|
||||
combatTracker = new CombatTracker(configService);
|
||||
antiAbuseService = new AntiAbuseService(configService, historyService);
|
||||
webhookService = new WebhookService(this, configService, messageService);
|
||||
webhookService.initialize();
|
||||
bountyService = new BountyService(this, configService, messageService, economyService, storageManager,
|
||||
historyService, playerCacheService, combatTracker, antiAbuseService, webhookService);
|
||||
bountyService.load();
|
||||
guiManager = new GuiManager(this, configService, messageService, bountyService, historyService);
|
||||
}
|
||||
|
||||
private void registerCommands() {
|
||||
BountyCommand bountyCommand = new BountyCommand(configService, messageService, bountyService, guiManager);
|
||||
PluginCommand bounty = Objects.requireNonNull(getCommand("bounty"), "bounty command missing from plugin.yml");
|
||||
bounty.setExecutor(bountyCommand);
|
||||
bounty.setTabCompleter(bountyCommand);
|
||||
|
||||
BountyAdminCommand adminCommand = new BountyAdminCommand(this, configService, messageService,
|
||||
bountyService, historyService, guiManager);
|
||||
PluginCommand admin = Objects.requireNonNull(getCommand("bountyadmin"), "bountyadmin command missing from plugin.yml");
|
||||
admin.setExecutor(adminCommand);
|
||||
admin.setTabCompleter(adminCommand);
|
||||
}
|
||||
|
||||
private void registerEvents() {
|
||||
Bukkit.getPluginManager().registerEvents(
|
||||
new BountyListener(this, guiManager, bountyService, playerCacheService, combatTracker), this);
|
||||
}
|
||||
|
||||
private void registerPlaceholders() {
|
||||
if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
placeholderExpansion = new DirtBountiesExpansion(bountyService, historyService, getDescription().getVersion());
|
||||
((DirtBountiesExpansion) placeholderExpansion).register();
|
||||
getLogger().info("Registered PlaceholderAPI placeholders.");
|
||||
} catch (NoClassDefFoundError | RuntimeException ex) {
|
||||
getLogger().log(Level.WARNING, "PlaceholderAPI was present but DirtBounties could not register placeholders.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleTasks() {
|
||||
long cleanupTicks = ticks(configService.main().getString("storage.cleanup-expired-interval"), 600_000L);
|
||||
if (cleanupTicks > 0L) {
|
||||
cleanupTask = Bukkit.getScheduler().runTaskTimer(this, () -> {
|
||||
int removed = bountyService.cleanupExpired();
|
||||
if (removed > 0 && configService.main().getBoolean("logging.console.admin-actions", true)) {
|
||||
getLogger().info("Cleaned up " + removed + " expired bounties.");
|
||||
}
|
||||
}, cleanupTicks, cleanupTicks);
|
||||
}
|
||||
|
||||
long autosaveTicks = ticks(configService.main().getString("storage.autosave-interval"), 300_000L);
|
||||
if (autosaveTicks > 0L) {
|
||||
autosaveTask = Bukkit.getScheduler().runTaskTimer(this, () -> {
|
||||
bountyService.save();
|
||||
historyService.save();
|
||||
playerCacheService.save();
|
||||
}, autosaveTicks, autosaveTicks);
|
||||
}
|
||||
}
|
||||
|
||||
private long ticks(String duration, long fallbackMillis) {
|
||||
long millis = TimeUtil.parseMillis(duration, fallbackMillis);
|
||||
if (millis <= 0L) {
|
||||
return 0L;
|
||||
}
|
||||
return Math.max(20L, millis / 50L);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
package com.dirtbagmc.dirtbounties.command;
|
||||
|
||||
import com.dirtbagmc.dirtbounties.DirtBountiesPlugin;
|
||||
import com.dirtbagmc.dirtbounties.config.ConfigService;
|
||||
import com.dirtbagmc.dirtbounties.gui.GuiManager;
|
||||
import com.dirtbagmc.dirtbounties.model.BountyHistoryRecord;
|
||||
import com.dirtbagmc.dirtbounties.model.SuspiciousActivity;
|
||||
import com.dirtbagmc.dirtbounties.service.BountyService;
|
||||
import com.dirtbagmc.dirtbounties.service.HistoryService;
|
||||
import com.dirtbagmc.dirtbounties.service.MessageService;
|
||||
import com.dirtbagmc.dirtbounties.util.TimeUtil;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.TabExecutor;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
public final class BountyAdminCommand implements TabExecutor {
|
||||
private final DirtBountiesPlugin plugin;
|
||||
private final ConfigService configService;
|
||||
private final MessageService messages;
|
||||
private final BountyService bountyService;
|
||||
private final HistoryService historyService;
|
||||
private final GuiManager guiManager;
|
||||
|
||||
public BountyAdminCommand(DirtBountiesPlugin plugin, ConfigService configService, MessageService messages,
|
||||
BountyService bountyService, HistoryService historyService, GuiManager guiManager) {
|
||||
this.plugin = plugin;
|
||||
this.configService = configService;
|
||||
this.messages = messages;
|
||||
this.bountyService = bountyService;
|
||||
this.historyService = historyService;
|
||||
this.guiManager = guiManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||
if (!sender.hasPermission("dirtbounties.admin")) {
|
||||
messages.send(sender, "no-permission");
|
||||
return true;
|
||||
}
|
||||
if (args.length == 0 || args[0].equalsIgnoreCase("help")) {
|
||||
messages.sendLines(sender, "admin-help", Map.of());
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (args[0].toLowerCase(Locale.ROOT)) {
|
||||
case "reload" -> {
|
||||
plugin.reloadDirtBounties();
|
||||
messages.send(sender, "reload-complete");
|
||||
}
|
||||
case "remove" -> remove(sender, args);
|
||||
case "clearall" -> clearAll(sender, args);
|
||||
case "set" -> set(sender, args);
|
||||
case "expire" -> expire(sender, args);
|
||||
case "history" -> history(sender, args);
|
||||
case "suspicious" -> suspicious(sender);
|
||||
case "gui" -> {
|
||||
if (sender instanceof Player player) {
|
||||
guiManager.openAdminMain(player);
|
||||
} else {
|
||||
messages.send(sender, "player-only");
|
||||
}
|
||||
}
|
||||
default -> messages.sendLines(sender, "admin-help", Map.of());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void remove(CommandSender sender, String[] args) {
|
||||
if (args.length < 2) {
|
||||
messages.send(sender, "invalid-player");
|
||||
return;
|
||||
}
|
||||
OfflinePlayer target = bountyService.resolveTarget(args[1]);
|
||||
double refund = configService.main().getDouble("refunds.on-admin-remove-percent", 100.0);
|
||||
bountyService.adminRemove(sender, target, "ADMIN_REMOVE", refund);
|
||||
}
|
||||
|
||||
private void clearAll(CommandSender sender, String[] args) {
|
||||
if (args.length < 2 || !args[1].equalsIgnoreCase("confirm")) {
|
||||
messages.send(sender, "bounty.clearall-warning");
|
||||
return;
|
||||
}
|
||||
int count = bountyService.clearAll(sender);
|
||||
messages.send(sender, "bounty.clearall-done", Map.of("count", String.valueOf(count)));
|
||||
}
|
||||
|
||||
private void set(CommandSender sender, String[] args) {
|
||||
if (args.length < 3) {
|
||||
messages.send(sender, "invalid-number");
|
||||
return;
|
||||
}
|
||||
OfflinePlayer target = bountyService.resolveTarget(args[1]);
|
||||
double amount;
|
||||
try {
|
||||
amount = Double.parseDouble(args[2].replace(",", ""));
|
||||
} catch (NumberFormatException ex) {
|
||||
messages.send(sender, "invalid-number");
|
||||
return;
|
||||
}
|
||||
bountyService.adminSet(sender, target, amount);
|
||||
}
|
||||
|
||||
private void expire(CommandSender sender, String[] args) {
|
||||
if (args.length < 2) {
|
||||
messages.send(sender, "invalid-player");
|
||||
return;
|
||||
}
|
||||
OfflinePlayer target = bountyService.resolveTarget(args[1]);
|
||||
double refund = configService.main().getDouble("refunds.on-expire-percent", 50.0);
|
||||
bountyService.adminRemove(sender, target, "EXPIRED", refund);
|
||||
}
|
||||
|
||||
private void history(CommandSender sender, String[] args) {
|
||||
if (args.length < 2) {
|
||||
messages.send(sender, "invalid-player");
|
||||
return;
|
||||
}
|
||||
OfflinePlayer target = bountyService.resolveTarget(args[1]);
|
||||
if (target == null) {
|
||||
messages.send(sender, "invalid-player");
|
||||
return;
|
||||
}
|
||||
int limit = configService.main().getInt("history.max-records-per-player-command", 10);
|
||||
List<BountyHistoryRecord> records = historyService.forPlayer(target.getUniqueId(), limit);
|
||||
if (records.isEmpty()) {
|
||||
messages.send(sender, "admin.history-empty", Map.of("target", target.getName() == null ? args[1] : target.getName()));
|
||||
return;
|
||||
}
|
||||
for (BountyHistoryRecord record : records) {
|
||||
messages.send(sender, "admin.history-line", Map.of(
|
||||
"time", TimeUtil.formatTimestamp(record.createdAt()),
|
||||
"type", record.type(),
|
||||
"amount", bountyService.formatAmount(record.amount()),
|
||||
"note", record.note()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
private void suspicious(CommandSender sender) {
|
||||
List<SuspiciousActivity> records = historyService.suspiciousRecent(10);
|
||||
if (records.isEmpty()) {
|
||||
messages.send(sender, "admin.suspicious-empty");
|
||||
return;
|
||||
}
|
||||
for (SuspiciousActivity record : records) {
|
||||
messages.send(sender, "admin.suspicious-line", Map.of(
|
||||
"time", TimeUtil.formatTimestamp(record.createdAt()),
|
||||
"type", record.type(),
|
||||
"details", record.details()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
|
||||
if (!sender.hasPermission("dirtbounties.admin")) {
|
||||
return List.of();
|
||||
}
|
||||
if (args.length == 1) {
|
||||
return filter(args[0], List.of("reload", "remove", "clearall", "set", "expire", "history", "suspicious", "gui", "help"));
|
||||
}
|
||||
if (args.length == 2 && List.of("remove", "set", "expire", "history").contains(args[0].toLowerCase(Locale.ROOT))) {
|
||||
List<String> names = new ArrayList<>();
|
||||
Bukkit.getOnlinePlayers().forEach(player -> names.add(player.getName()));
|
||||
bountyService.activeBounties().forEach(bounty -> names.add(bounty.targetName()));
|
||||
return filter(args[1], names);
|
||||
}
|
||||
if (args.length == 2 && args[0].equalsIgnoreCase("clearall")) {
|
||||
return filter(args[1], List.of("confirm"));
|
||||
}
|
||||
if (args.length == 3 && args[0].equalsIgnoreCase("set")) {
|
||||
return filter(args[2], List.of("100", "1000", "5000", "10000"));
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
private List<String> filter(String input, List<String> options) {
|
||||
String lower = input.toLowerCase(Locale.ROOT);
|
||||
return options.stream()
|
||||
.distinct()
|
||||
.filter(option -> option.toLowerCase(Locale.ROOT).startsWith(lower))
|
||||
.sorted(String.CASE_INSENSITIVE_ORDER)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
package com.dirtbagmc.dirtbounties.command;
|
||||
|
||||
import com.dirtbagmc.dirtbounties.config.ConfigService;
|
||||
import com.dirtbagmc.dirtbounties.gui.GuiManager;
|
||||
import com.dirtbagmc.dirtbounties.model.Bounty;
|
||||
import com.dirtbagmc.dirtbounties.service.BountyService;
|
||||
import com.dirtbagmc.dirtbounties.service.MessageService;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.TabExecutor;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
public final class BountyCommand implements TabExecutor {
|
||||
private final ConfigService configService;
|
||||
private final MessageService messages;
|
||||
private final BountyService bountyService;
|
||||
private final GuiManager guiManager;
|
||||
|
||||
public BountyCommand(ConfigService configService, MessageService messages,
|
||||
BountyService bountyService, GuiManager guiManager) {
|
||||
this.configService = configService;
|
||||
this.messages = messages;
|
||||
this.bountyService = bountyService;
|
||||
this.guiManager = guiManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||
if (!sender.hasPermission("dirtbounties.use")) {
|
||||
messages.send(sender, "no-permission");
|
||||
return true;
|
||||
}
|
||||
if (args.length == 0 || args[0].equalsIgnoreCase("gui")) {
|
||||
if (!(sender instanceof Player player)) {
|
||||
messages.send(sender, "player-only");
|
||||
return true;
|
||||
}
|
||||
guiManager.openMain(player, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (args[0].toLowerCase(Locale.ROOT)) {
|
||||
case "help" -> messages.sendLines(sender, "help", Map.of());
|
||||
case "place" -> place(sender, args, false);
|
||||
case "add", "increase" -> place(sender, args, true);
|
||||
case "list" -> list(sender);
|
||||
case "top" -> top(sender);
|
||||
case "view" -> view(sender, args);
|
||||
case "claiminfo" -> claimInfo(sender, args);
|
||||
default -> messages.send(sender, "unknown-command");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void place(CommandSender sender, String[] args, boolean increase) {
|
||||
if (!(sender instanceof Player player)) {
|
||||
messages.send(sender, "player-only");
|
||||
return;
|
||||
}
|
||||
if (!player.hasPermission(increase ? "dirtbounties.add" : "dirtbounties.place")) {
|
||||
messages.send(player, "no-permission");
|
||||
return;
|
||||
}
|
||||
if (args.length < 3) {
|
||||
messages.sendLines(player, "help", Map.of());
|
||||
return;
|
||||
}
|
||||
OfflinePlayer target = bountyService.resolveTarget(args[1]);
|
||||
double amount;
|
||||
try {
|
||||
amount = Double.parseDouble(args[2].replace(",", ""));
|
||||
} catch (NumberFormatException ex) {
|
||||
messages.send(player, "invalid-number");
|
||||
return;
|
||||
}
|
||||
ReasonInput reason = parseReason(args, 3);
|
||||
bountyService.placeBounty(player, target, amount, reason.reason(), reason.anonymous(), increase);
|
||||
}
|
||||
|
||||
private void list(CommandSender sender) {
|
||||
if (!sender.hasPermission("dirtbounties.view")) {
|
||||
messages.send(sender, "no-permission");
|
||||
return;
|
||||
}
|
||||
List<Bounty> bounties = bountyService.activeSorted();
|
||||
if (bounties.isEmpty()) {
|
||||
messages.send(sender, "no-active-bounties");
|
||||
return;
|
||||
}
|
||||
int limit = Math.min(10, bounties.size());
|
||||
for (int i = 0; i < limit; i++) {
|
||||
Bounty bounty = bounties.get(i);
|
||||
messages.send(sender, "bounty.list-line", bountyService.bountyPlaceholders(bounty));
|
||||
}
|
||||
}
|
||||
|
||||
private void top(CommandSender sender) {
|
||||
if (!sender.hasPermission("dirtbounties.top")) {
|
||||
messages.send(sender, "no-permission");
|
||||
return;
|
||||
}
|
||||
List<Bounty> bounties = bountyService.topBounties(10);
|
||||
if (bounties.isEmpty()) {
|
||||
messages.send(sender, "no-active-bounties");
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < bounties.size(); i++) {
|
||||
Bounty bounty = bounties.get(i);
|
||||
Map<String, String> placeholders = bountyService.bountyPlaceholders(bounty);
|
||||
placeholders.put("rank", String.valueOf(i + 1));
|
||||
messages.send(sender, "bounty.top-line", placeholders);
|
||||
}
|
||||
}
|
||||
|
||||
private void view(CommandSender sender, String[] args) {
|
||||
if (!sender.hasPermission("dirtbounties.view")) {
|
||||
messages.send(sender, "no-permission");
|
||||
return;
|
||||
}
|
||||
if (args.length < 2) {
|
||||
messages.send(sender, "invalid-player");
|
||||
return;
|
||||
}
|
||||
OfflinePlayer target = bountyService.resolveTarget(args[1]);
|
||||
if (target == null) {
|
||||
messages.send(sender, "invalid-player");
|
||||
return;
|
||||
}
|
||||
Bounty bounty = bountyService.bounty(target.getUniqueId()).orElse(null);
|
||||
if (bounty == null) {
|
||||
messages.send(sender, "bounty-not-found", Map.of("target", target.getName() == null ? args[1] : target.getName()));
|
||||
return;
|
||||
}
|
||||
messages.sendLines(sender, "bounty.view", bountyService.bountyPlaceholders(bounty));
|
||||
if (sender instanceof Player player) {
|
||||
guiManager.openDetail(player, target.getUniqueId());
|
||||
}
|
||||
}
|
||||
|
||||
private void claimInfo(CommandSender sender, String[] args) {
|
||||
if (!sender.hasPermission("dirtbounties.view")) {
|
||||
messages.send(sender, "no-permission");
|
||||
return;
|
||||
}
|
||||
if (args.length < 2) {
|
||||
messages.send(sender, "invalid-player");
|
||||
return;
|
||||
}
|
||||
OfflinePlayer target = bountyService.resolveTarget(args[1]);
|
||||
if (target == null) {
|
||||
messages.send(sender, "invalid-player");
|
||||
return;
|
||||
}
|
||||
Map<String, String> placeholders = new HashMap<>();
|
||||
placeholders.put("target", target.getName() == null ? args[1] : target.getName());
|
||||
placeholders.put("pvp", yes(configService.main().getBoolean("claim-rules.require-pvp-kill", true)));
|
||||
placeholders.put("same_ip", yes(configService.main().getBoolean("claim-rules.prevent-same-ip-claims", true)));
|
||||
placeholders.put("worlds", configService.main().getString("claim-rules.worlds.mode", "blacklist")
|
||||
+ " " + configService.main().getStringList("claim-rules.worlds.list"));
|
||||
placeholders.put("combat", yes(configService.main().getBoolean("claim-rules.combat.enabled", true)));
|
||||
messages.sendLines(sender, "bounty.claiminfo", placeholders);
|
||||
}
|
||||
|
||||
private ReasonInput parseReason(String[] args, int start) {
|
||||
boolean anonymous = false;
|
||||
List<String> parts = new ArrayList<>();
|
||||
for (int i = start; i < args.length; i++) {
|
||||
if (args[i].equalsIgnoreCase("--anonymous") || args[i].equalsIgnoreCase("-a")) {
|
||||
anonymous = true;
|
||||
} else {
|
||||
parts.add(args[i]);
|
||||
}
|
||||
}
|
||||
String reason = parts.isEmpty()
|
||||
? configService.main().getString("bounties.default-reason", "No reason given.")
|
||||
: String.join(" ", parts);
|
||||
return new ReasonInput(reason, anonymous);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
|
||||
if (args.length == 1) {
|
||||
return filter(args[0], List.of("help", "place", "add", "list", "top", "view", "claiminfo", "gui"));
|
||||
}
|
||||
if (args.length == 2 && List.of("place", "add", "increase", "view", "claiminfo").contains(args[0].toLowerCase(Locale.ROOT))) {
|
||||
List<String> names = new ArrayList<>();
|
||||
Bukkit.getOnlinePlayers().forEach(player -> names.add(player.getName()));
|
||||
bountyService.activeBounties().forEach(bounty -> names.add(bounty.targetName()));
|
||||
return filter(args[1], names);
|
||||
}
|
||||
if (args.length == 3 && List.of("place", "add", "increase").contains(args[0].toLowerCase(Locale.ROOT))) {
|
||||
return filter(args[2], List.of("100", "500", "1000", "5000", "10000"));
|
||||
}
|
||||
if (args.length >= 4 && List.of("place", "add", "increase").contains(args[0].toLowerCase(Locale.ROOT))) {
|
||||
return filter(args[args.length - 1], List.of("--anonymous"));
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
private List<String> filter(String input, List<String> options) {
|
||||
String lower = input.toLowerCase(Locale.ROOT);
|
||||
return options.stream()
|
||||
.distinct()
|
||||
.filter(option -> option.toLowerCase(Locale.ROOT).startsWith(lower))
|
||||
.sorted(String.CASE_INSENSITIVE_ORDER)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private String yes(boolean value) {
|
||||
return value ? "yes" : "no";
|
||||
}
|
||||
|
||||
private record ReasonInput(String reason, boolean anonymous) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.dirtbagmc.dirtbounties.config;
|
||||
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public final class ConfigService {
|
||||
private final JavaPlugin plugin;
|
||||
private FileConfiguration mainConfig;
|
||||
private YamlConfiguration messagesConfig;
|
||||
private YamlConfiguration guiConfig;
|
||||
private File messagesFile;
|
||||
private File guiFile;
|
||||
|
||||
public ConfigService(JavaPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public void load() {
|
||||
plugin.getDataFolder().mkdirs();
|
||||
plugin.saveDefaultConfig();
|
||||
saveResourceIfMissing("messages.yml");
|
||||
saveResourceIfMissing("gui.yml");
|
||||
|
||||
plugin.reloadConfig();
|
||||
mainConfig = plugin.getConfig();
|
||||
messagesFile = new File(plugin.getDataFolder(), "messages.yml");
|
||||
guiFile = new File(plugin.getDataFolder(), "gui.yml");
|
||||
messagesConfig = YamlConfiguration.loadConfiguration(messagesFile);
|
||||
guiConfig = YamlConfiguration.loadConfiguration(guiFile);
|
||||
}
|
||||
|
||||
public FileConfiguration main() {
|
||||
return mainConfig;
|
||||
}
|
||||
|
||||
public YamlConfiguration messages() {
|
||||
return messagesConfig;
|
||||
}
|
||||
|
||||
public YamlConfiguration gui() {
|
||||
return guiConfig;
|
||||
}
|
||||
|
||||
public boolean debug() {
|
||||
return mainConfig.getBoolean("server.debug", false);
|
||||
}
|
||||
|
||||
public ConfigurationSection guiSection(String path) {
|
||||
return guiConfig.getConfigurationSection(path);
|
||||
}
|
||||
|
||||
private void saveResourceIfMissing(String name) {
|
||||
File file = new File(plugin.getDataFolder(), name);
|
||||
if (!file.exists()) {
|
||||
plugin.saveResource(name, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package com.dirtbagmc.dirtbounties.economy;
|
||||
|
||||
import com.dirtbagmc.dirtbounties.config.ConfigService;
|
||||
import com.dirtbagmc.dirtbounties.util.NumberUtil;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.plugin.RegisteredServiceProvider;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public final class EconomyService {
|
||||
private final JavaPlugin plugin;
|
||||
private final ConfigService configService;
|
||||
private Class<?> economyClass;
|
||||
private Object provider;
|
||||
private boolean ready;
|
||||
private String providerName = "None";
|
||||
|
||||
public EconomyService(JavaPlugin plugin, ConfigService configService) {
|
||||
this.plugin = plugin;
|
||||
this.configService = configService;
|
||||
}
|
||||
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
public void initialize() {
|
||||
ready = false;
|
||||
provider = null;
|
||||
providerName = "None";
|
||||
if (!configService.main().getBoolean("economy.enabled", true)) {
|
||||
plugin.getLogger().info("Economy integration is disabled in config.yml.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
economyClass = Class.forName("net.milkbowl.vault.economy.Economy", false, plugin.getClass().getClassLoader());
|
||||
RegisteredServiceProvider registration = Bukkit.getServicesManager().getRegistration((Class) economyClass);
|
||||
if (registration == null || registration.getProvider() == null) {
|
||||
logMissingProvider();
|
||||
return;
|
||||
}
|
||||
provider = registration.getProvider();
|
||||
providerName = provider.getClass().getSimpleName();
|
||||
ready = true;
|
||||
if (configService.main().getBoolean("economy.provider-log-on-enable", true)) {
|
||||
plugin.getLogger().info("Hooked economy provider through Vault services: " + providerName);
|
||||
}
|
||||
} catch (ClassNotFoundException | LinkageError ex) {
|
||||
logMissingProvider();
|
||||
} catch (RuntimeException ex) {
|
||||
plugin.getLogger().log(Level.WARNING, "Could not hook an economy provider.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isReady() {
|
||||
return ready && provider != null;
|
||||
}
|
||||
|
||||
public String providerName() {
|
||||
return providerName;
|
||||
}
|
||||
|
||||
public double balance(OfflinePlayer player) {
|
||||
if (!isReady()) {
|
||||
return 0.0;
|
||||
}
|
||||
Object result = invoke("getBalance", new Class<?>[]{OfflinePlayer.class}, player);
|
||||
return result instanceof Number number ? number.doubleValue() : 0.0;
|
||||
}
|
||||
|
||||
public EconomyResult withdraw(OfflinePlayer player, double amount) {
|
||||
return transaction("withdrawPlayer", player, amount);
|
||||
}
|
||||
|
||||
public EconomyResult deposit(OfflinePlayer player, double amount) {
|
||||
return transaction("depositPlayer", player, amount);
|
||||
}
|
||||
|
||||
public String format(double amount) {
|
||||
double clamped = NumberUtil.clampMoney(amount);
|
||||
if (isReady()) {
|
||||
Object result = invoke("format", new Class<?>[]{double.class}, clamped);
|
||||
if (result instanceof String formatted && !formatted.isBlank()) {
|
||||
return formatted;
|
||||
}
|
||||
}
|
||||
return configService.main().getString("economy.currency-format", "${amount}")
|
||||
.replace("{amount}", NumberUtil.compact(clamped));
|
||||
}
|
||||
|
||||
private EconomyResult transaction(String method, OfflinePlayer player, double amount) {
|
||||
if (!isReady()) {
|
||||
return EconomyResult.failure("Economy provider is missing.");
|
||||
}
|
||||
if (amount <= 0.0) {
|
||||
return EconomyResult.ok();
|
||||
}
|
||||
Object response = invoke(method, new Class<?>[]{OfflinePlayer.class, double.class}, player, NumberUtil.clampMoney(amount));
|
||||
if (response == null) {
|
||||
return EconomyResult.failure("Economy provider returned no response.");
|
||||
}
|
||||
try {
|
||||
Method success = response.getClass().getMethod("transactionSuccess");
|
||||
boolean ok = Boolean.TRUE.equals(success.invoke(response));
|
||||
if (ok) {
|
||||
return EconomyResult.ok();
|
||||
}
|
||||
Method errorMessage = response.getClass().getMethod("errorMessage");
|
||||
Object message = errorMessage.invoke(response);
|
||||
return EconomyResult.failure(message == null ? "Economy transaction failed." : message.toString());
|
||||
} catch (ReflectiveOperationException ex) {
|
||||
return EconomyResult.failure("Could not read economy transaction response.");
|
||||
}
|
||||
}
|
||||
|
||||
private Object invoke(String methodName, Class<?>[] parameterTypes, Object... args) {
|
||||
try {
|
||||
Method method = provider.getClass().getMethod(methodName, parameterTypes);
|
||||
return method.invoke(provider, args);
|
||||
} catch (NoSuchMethodException ex) {
|
||||
plugin.getLogger().warning("Economy provider does not support method: " + methodName);
|
||||
} catch (IllegalAccessException | InvocationTargetException ex) {
|
||||
plugin.getLogger().log(Level.WARNING, "Economy method failed: " + methodName, ex);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void logMissingProvider() {
|
||||
String message = "No Vault-compatible economy provider is registered. Bounty economy actions will be blocked.";
|
||||
if (configService.main().getBoolean("economy.fail-if-missing", false)) {
|
||||
plugin.getLogger().severe(message);
|
||||
} else if (configService.main().getBoolean("logging.console.economy-status", true)) {
|
||||
plugin.getLogger().warning(message + " This also covers missing CMI Vault injector setup.");
|
||||
}
|
||||
}
|
||||
|
||||
public record EconomyResult(boolean success, String message) {
|
||||
public static EconomyResult ok() {
|
||||
return new EconomyResult(true, "");
|
||||
}
|
||||
|
||||
public static EconomyResult failure(String message) {
|
||||
return new EconomyResult(false, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.dirtbagmc.dirtbounties.gui;
|
||||
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.InventoryHolder;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public final class GuiHolder implements InventoryHolder {
|
||||
private final GuiType type;
|
||||
private final int page;
|
||||
private final UUID targetUuid;
|
||||
private Inventory inventory;
|
||||
|
||||
public GuiHolder(GuiType type, int page, UUID targetUuid) {
|
||||
this.type = type;
|
||||
this.page = page;
|
||||
this.targetUuid = targetUuid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Inventory getInventory() {
|
||||
return inventory;
|
||||
}
|
||||
|
||||
public void inventory(Inventory inventory) {
|
||||
this.inventory = inventory;
|
||||
}
|
||||
|
||||
public GuiType type() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public int page() {
|
||||
return page;
|
||||
}
|
||||
|
||||
public UUID targetUuid() {
|
||||
return targetUuid;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,589 @@
|
||||
package com.dirtbagmc.dirtbounties.gui;
|
||||
|
||||
import com.dirtbagmc.dirtbounties.config.ConfigService;
|
||||
import com.dirtbagmc.dirtbounties.model.Bounty;
|
||||
import com.dirtbagmc.dirtbounties.model.BountyContribution;
|
||||
import com.dirtbagmc.dirtbounties.model.BountyHistoryRecord;
|
||||
import com.dirtbagmc.dirtbounties.model.SuspiciousActivity;
|
||||
import com.dirtbagmc.dirtbounties.service.BountyService;
|
||||
import com.dirtbagmc.dirtbounties.service.HistoryService;
|
||||
import com.dirtbagmc.dirtbounties.service.MessageService;
|
||||
import com.dirtbagmc.dirtbounties.util.ItemBuilder;
|
||||
import com.dirtbagmc.dirtbounties.util.NumberUtil;
|
||||
import com.dirtbagmc.dirtbounties.util.TimeUtil;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class GuiManager {
|
||||
private final JavaPlugin plugin;
|
||||
private final ConfigService configService;
|
||||
private final MessageService messages;
|
||||
private final BountyService bountyService;
|
||||
private final HistoryService historyService;
|
||||
private final Map<UUID, PendingBountyInput> pendingInputs = new HashMap<>();
|
||||
|
||||
public GuiManager(JavaPlugin plugin, ConfigService configService, MessageService messages,
|
||||
BountyService bountyService, HistoryService historyService) {
|
||||
this.plugin = plugin;
|
||||
this.configService = configService;
|
||||
this.messages = messages;
|
||||
this.bountyService = bountyService;
|
||||
this.historyService = historyService;
|
||||
}
|
||||
|
||||
public void openMain(Player player, int page) {
|
||||
if (!enabled(player)) {
|
||||
return;
|
||||
}
|
||||
List<Bounty> bounties = bountyService.activeSorted();
|
||||
Inventory inventory = create(GuiType.MAIN, page, null, "titles.main", Map.of());
|
||||
decorate(inventory);
|
||||
fillBounties(inventory, bounties, page, false);
|
||||
addMainButtons(inventory, page, bounties.size(), GuiType.MAIN, player);
|
||||
player.openInventory(inventory);
|
||||
messages.play(player, "gui.open-sound");
|
||||
}
|
||||
|
||||
public void openTop(Player player, int page) {
|
||||
if (!enabled(player)) {
|
||||
return;
|
||||
}
|
||||
List<Bounty> bounties = bountyService.activeSorted();
|
||||
Inventory inventory = create(GuiType.TOP, page, null, "titles.top", Map.of());
|
||||
decorate(inventory);
|
||||
fillBounties(inventory, bounties, page, true);
|
||||
addMainButtons(inventory, page, bounties.size(), GuiType.TOP, player);
|
||||
player.openInventory(inventory);
|
||||
messages.play(player, "gui.open-sound");
|
||||
}
|
||||
|
||||
public void openDetail(Player player, UUID targetUuid) {
|
||||
Bounty bounty = bountyService.bounty(targetUuid).orElse(null);
|
||||
if (bounty == null) {
|
||||
messages.send(player, "bounty-not-found", Map.of("target", "Unknown"));
|
||||
openMain(player, 0);
|
||||
return;
|
||||
}
|
||||
Map<String, String> placeholders = bountyService.bountyPlaceholders(bounty);
|
||||
placeholders.put("reason_lines", reasonLines(bounty));
|
||||
placeholders.putAll(claimRulePlaceholders());
|
||||
|
||||
Inventory inventory = create(GuiType.DETAIL, 0, targetUuid, "titles.detail", placeholders);
|
||||
decorate(inventory);
|
||||
OfflinePlayer owner = Bukkit.getOfflinePlayer(targetUuid);
|
||||
inventory.setItem(13, item("items.detail-head", placeholders, owner));
|
||||
inventory.setItem(22, item("items.claim-info", placeholders, null));
|
||||
inventory.setItem(30, item("items.add", placeholders, null));
|
||||
inventory.setItem(slot("layout.back-slot", 46), item("items.back", placeholders, null));
|
||||
inventory.setItem(slot("layout.refresh-slot", 49), item("items.refresh", placeholders, null));
|
||||
player.openInventory(inventory);
|
||||
messages.play(player, "gui.open-sound");
|
||||
}
|
||||
|
||||
public void openConfirm(Player player, PendingBountyInput input) {
|
||||
Map<String, String> placeholders = confirmPlaceholders(input);
|
||||
Inventory inventory = create(GuiType.CONFIRM, 0, input.targetUuid(), "titles.confirm", placeholders);
|
||||
decorate(inventory);
|
||||
OfflinePlayer owner = input.targetUuid() == null ? null : Bukkit.getOfflinePlayer(input.targetUuid());
|
||||
inventory.setItem(13, ItemBuilder.simple(Material.PLAYER_HEAD, "&c&l{target}",
|
||||
List.of("&7Amount: &f{amount}", "&7Total cost: &f{cost}", "&7Reason: &f{reason}"),
|
||||
messages, placeholders));
|
||||
if (owner != null && inventory.getItem(13) != null) {
|
||||
inventory.setItem(13, item("items.bounty", placeholders, owner));
|
||||
}
|
||||
inventory.setItem(29, item("items.confirm", placeholders, owner));
|
||||
inventory.setItem(31, anonymousToggle(input));
|
||||
inventory.setItem(33, item("items.cancel", placeholders, null));
|
||||
player.openInventory(inventory);
|
||||
messages.play(player, "gui.open-sound");
|
||||
}
|
||||
|
||||
public void openAdminMain(Player player) {
|
||||
if (!player.hasPermission("dirtbounties.admin")) {
|
||||
messages.send(player, "no-permission");
|
||||
return;
|
||||
}
|
||||
Inventory inventory = create(GuiType.ADMIN_MAIN, 0, null, "titles.admin-main", Map.of());
|
||||
decorate(inventory);
|
||||
inventory.setItem(20, item("items.admin-active", Map.of(), null));
|
||||
inventory.setItem(22, item("items.admin-history", Map.of(), null));
|
||||
inventory.setItem(24, item("items.admin-suspicious", Map.of(), null));
|
||||
inventory.setItem(slot("layout.back-slot", 46), item("items.back", Map.of(), null));
|
||||
player.openInventory(inventory);
|
||||
messages.send(player, "gui.admin-opened");
|
||||
messages.play(player, "gui.open-sound");
|
||||
}
|
||||
|
||||
public void openAdminHistory(Player player, int page) {
|
||||
Inventory inventory = create(GuiType.ADMIN_HISTORY, page, null, "titles.admin-history", Map.of());
|
||||
decorate(inventory);
|
||||
List<Integer> slots = contentSlots();
|
||||
List<BountyHistoryRecord> records = historyService.recent(500);
|
||||
int start = page * slots.size();
|
||||
for (int i = 0; i < slots.size() && start + i < records.size(); i++) {
|
||||
BountyHistoryRecord record = records.get(start + i);
|
||||
Map<String, String> placeholders = new HashMap<>();
|
||||
placeholders.put("type", record.type());
|
||||
placeholders.put("target", record.targetName());
|
||||
placeholders.put("actor", record.actorName());
|
||||
placeholders.put("amount", bountyService.formatAmount(record.amount()));
|
||||
placeholders.put("time", TimeUtil.formatTimestamp(record.createdAt()));
|
||||
placeholders.put("note", record.note());
|
||||
inventory.setItem(slots.get(i), ItemBuilder.simple(Material.PAPER, "&e{type} &8| &f{target}",
|
||||
List.of("&7Actor: &f{actor}", "&7Amount: &f{amount}", "&7When: &f{time}", "&7Note: &f{note}"),
|
||||
messages, placeholders));
|
||||
}
|
||||
addPageButtons(inventory, page, records.size(), GuiType.ADMIN_HISTORY);
|
||||
inventory.setItem(slot("layout.back-slot", 46), item("items.back", Map.of(), null));
|
||||
player.openInventory(inventory);
|
||||
}
|
||||
|
||||
public void openAdminSuspicious(Player player, int page) {
|
||||
Inventory inventory = create(GuiType.ADMIN_SUSPICIOUS, page, null, "titles.admin-suspicious", Map.of());
|
||||
decorate(inventory);
|
||||
List<Integer> slots = contentSlots();
|
||||
List<SuspiciousActivity> records = historyService.suspiciousRecent(500);
|
||||
int start = page * slots.size();
|
||||
for (int i = 0; i < slots.size() && start + i < records.size(); i++) {
|
||||
SuspiciousActivity record = records.get(start + i);
|
||||
Map<String, String> placeholders = new HashMap<>();
|
||||
placeholders.put("type", record.type());
|
||||
placeholders.put("killer", record.killerName());
|
||||
placeholders.put("target", record.targetName());
|
||||
placeholders.put("severity", String.valueOf(record.severity()));
|
||||
placeholders.put("time", TimeUtil.formatTimestamp(record.createdAt()));
|
||||
placeholders.put("details", record.details());
|
||||
inventory.setItem(slots.get(i), ItemBuilder.simple(Material.REDSTONE_TORCH, "&c{type} &8| &fSeverity {severity}",
|
||||
List.of("&7Killer: &f{killer}", "&7Target: &f{target}", "&7When: &f{time}", "&7Details: &f{details}"),
|
||||
messages, placeholders));
|
||||
}
|
||||
addPageButtons(inventory, page, records.size(), GuiType.ADMIN_SUSPICIOUS);
|
||||
inventory.setItem(slot("layout.back-slot", 46), item("items.back", Map.of(), null));
|
||||
player.openInventory(inventory);
|
||||
}
|
||||
|
||||
public void handleClick(InventoryClickEvent event) {
|
||||
if (!(event.getInventory().getHolder() instanceof GuiHolder holder)) {
|
||||
return;
|
||||
}
|
||||
event.setCancelled(true);
|
||||
if (!(event.getWhoClicked() instanceof Player player)) {
|
||||
return;
|
||||
}
|
||||
if (event.getRawSlot() >= event.getView().getTopInventory().getSize()) {
|
||||
return;
|
||||
}
|
||||
messages.play(player, "gui.click-sound");
|
||||
int slot = event.getRawSlot();
|
||||
switch (holder.type()) {
|
||||
case MAIN, TOP -> handleBountyListClick(player, holder, slot);
|
||||
case DETAIL -> handleDetailClick(player, holder, slot);
|
||||
case CONFIRM -> handleConfirmClick(player, slot);
|
||||
case ADMIN_MAIN -> handleAdminMainClick(player, slot);
|
||||
case ADMIN_HISTORY -> handlePagedAdminClick(player, holder, slot, GuiType.ADMIN_HISTORY);
|
||||
case ADMIN_SUSPICIOUS -> handlePagedAdminClick(player, holder, slot, GuiType.ADMIN_SUSPICIOUS);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasPendingInput(UUID playerUuid) {
|
||||
return pendingInputs.containsKey(playerUuid);
|
||||
}
|
||||
|
||||
public void startPlaceFlow(Player player) {
|
||||
if (!enabled(player)) {
|
||||
return;
|
||||
}
|
||||
PendingBountyInput input = new PendingBountyInput(player.getUniqueId(), PendingBountyInput.Stage.TARGET, inputExpiresAt());
|
||||
pendingInputs.put(player.getUniqueId(), input);
|
||||
player.closeInventory();
|
||||
messages.send(player, "gui.prompt-target");
|
||||
}
|
||||
|
||||
public void startAddFlow(Player player, Bounty bounty) {
|
||||
PendingBountyInput input = new PendingBountyInput(player.getUniqueId(), PendingBountyInput.Stage.AMOUNT, inputExpiresAt());
|
||||
input.targetUuid(bounty.targetUuid());
|
||||
input.targetName(bounty.targetName());
|
||||
input.increase(true);
|
||||
pendingInputs.put(player.getUniqueId(), input);
|
||||
player.closeInventory();
|
||||
messages.send(player, "gui.prompt-amount", Map.of("target", bounty.targetName()));
|
||||
}
|
||||
|
||||
public void acceptChatInput(Player player, String message) {
|
||||
PendingBountyInput input = pendingInputs.get(player.getUniqueId());
|
||||
if (input == null) {
|
||||
return;
|
||||
}
|
||||
if (System.currentTimeMillis() > input.expiresAt()) {
|
||||
pendingInputs.remove(player.getUniqueId());
|
||||
messages.send(player, "gui.input-expired");
|
||||
return;
|
||||
}
|
||||
if (message.equalsIgnoreCase("cancel")) {
|
||||
pendingInputs.remove(player.getUniqueId());
|
||||
messages.send(player, "gui.input-cancelled");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (input.stage()) {
|
||||
case TARGET -> acceptTarget(player, input, message);
|
||||
case AMOUNT -> acceptAmount(player, input, message);
|
||||
case REASON -> acceptReason(player, input, message);
|
||||
case CONFIRM -> openConfirm(player, input);
|
||||
}
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
pendingInputs.clear();
|
||||
}
|
||||
|
||||
private void acceptTarget(Player player, PendingBountyInput input, String message) {
|
||||
OfflinePlayer target = bountyService.resolveTarget(message);
|
||||
if (target == null) {
|
||||
messages.send(player, "invalid-player");
|
||||
messages.send(player, "gui.prompt-target");
|
||||
return;
|
||||
}
|
||||
input.targetUuid(target.getUniqueId());
|
||||
input.targetName(target.getName() == null ? message : target.getName());
|
||||
input.stage(PendingBountyInput.Stage.AMOUNT);
|
||||
input.expiresAt(inputExpiresAt());
|
||||
messages.send(player, "gui.prompt-amount", Map.of("target", input.targetName()));
|
||||
}
|
||||
|
||||
private void acceptAmount(Player player, PendingBountyInput input, String message) {
|
||||
double amount;
|
||||
try {
|
||||
amount = NumberUtil.clampMoney(Double.parseDouble(message.replace(",", "")));
|
||||
} catch (NumberFormatException ex) {
|
||||
messages.send(player, "invalid-number");
|
||||
messages.send(player, "gui.prompt-amount", Map.of("target", input.targetName()));
|
||||
return;
|
||||
}
|
||||
input.amount(amount);
|
||||
input.expiresAt(inputExpiresAt());
|
||||
if (configService.main().getBoolean("bounties.allow-reasons", true)) {
|
||||
input.stage(PendingBountyInput.Stage.REASON);
|
||||
messages.send(player, "gui.prompt-reason", Map.of("target", input.targetName()));
|
||||
} else {
|
||||
input.reason(configService.main().getString("bounties.default-reason", "No reason given."));
|
||||
input.stage(PendingBountyInput.Stage.CONFIRM);
|
||||
openConfirm(player, input);
|
||||
}
|
||||
}
|
||||
|
||||
private void acceptReason(Player player, PendingBountyInput input, String message) {
|
||||
input.reason(message.equals("-") ? configService.main().getString("bounties.default-reason", "No reason given.") : message);
|
||||
input.stage(PendingBountyInput.Stage.CONFIRM);
|
||||
input.expiresAt(inputExpiresAt());
|
||||
messages.send(player, "gui.confirm-opened");
|
||||
openConfirm(player, input);
|
||||
}
|
||||
|
||||
private void handleBountyListClick(Player player, GuiHolder holder, int slot) {
|
||||
if (slot == slot("layout.place-slot", 48)) {
|
||||
startPlaceFlow(player);
|
||||
return;
|
||||
}
|
||||
if (slot == slot("layout.top-slot", 50)) {
|
||||
if (holder.type() == GuiType.TOP) {
|
||||
openMain(player, 0);
|
||||
} else {
|
||||
openTop(player, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (slot == slot("layout.refresh-slot", 49)) {
|
||||
if (holder.type() == GuiType.TOP) {
|
||||
openTop(player, holder.page());
|
||||
} else {
|
||||
openMain(player, holder.page());
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (slot == slot("layout.previous-slot", 45) && holder.page() > 0) {
|
||||
if (holder.type() == GuiType.TOP) {
|
||||
openTop(player, holder.page() - 1);
|
||||
} else {
|
||||
openMain(player, holder.page() - 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (slot == slot("layout.next-slot", 53)) {
|
||||
if (holder.type() == GuiType.TOP) {
|
||||
openTop(player, holder.page() + 1);
|
||||
} else {
|
||||
openMain(player, holder.page() + 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (slot == slot("layout.admin-slot", 52) && player.hasPermission("dirtbounties.admin")) {
|
||||
openAdminMain(player);
|
||||
return;
|
||||
}
|
||||
Bounty clicked = bountyAt(holder.page(), slot);
|
||||
if (clicked != null) {
|
||||
openDetail(player, clicked.targetUuid());
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDetailClick(Player player, GuiHolder holder, int slot) {
|
||||
if (slot == slot("layout.back-slot", 46)) {
|
||||
openMain(player, 0);
|
||||
return;
|
||||
}
|
||||
if (slot == slot("layout.refresh-slot", 49)) {
|
||||
openDetail(player, holder.targetUuid());
|
||||
return;
|
||||
}
|
||||
if (slot == 30) {
|
||||
Bounty bounty = bountyService.bounty(holder.targetUuid()).orElse(null);
|
||||
if (bounty != null) {
|
||||
startAddFlow(player, bounty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleConfirmClick(Player player, int slot) {
|
||||
PendingBountyInput input = pendingInputs.get(player.getUniqueId());
|
||||
if (input == null) {
|
||||
player.closeInventory();
|
||||
return;
|
||||
}
|
||||
if (slot == 31) {
|
||||
if (!configService.main().getBoolean("bounties.allow-anonymous", true)
|
||||
|| (configService.main().getBoolean("bounties.anonymous-requires-permission", true)
|
||||
&& !player.hasPermission("dirtbounties.anonymous"))) {
|
||||
messages.send(player, "no-permission");
|
||||
messages.play(player, "gui.error-sound");
|
||||
return;
|
||||
}
|
||||
input.anonymous(!input.anonymous());
|
||||
openConfirm(player, input);
|
||||
return;
|
||||
}
|
||||
if (slot == 33) {
|
||||
pendingInputs.remove(player.getUniqueId());
|
||||
messages.send(player, "gui.input-cancelled");
|
||||
player.closeInventory();
|
||||
return;
|
||||
}
|
||||
if (slot == 29) {
|
||||
OfflinePlayer target = input.targetUuid() == null ? bountyService.resolveTarget(input.targetName()) : Bukkit.getOfflinePlayer(input.targetUuid());
|
||||
boolean success = bountyService.placeBounty(player, target, input.amount(), input.reason(), input.anonymous(), input.increase());
|
||||
if (success) {
|
||||
pendingInputs.remove(player.getUniqueId());
|
||||
messages.play(player, "gui.success-sound");
|
||||
if (configService.main().getBoolean("gui.close-on-confirm", true)) {
|
||||
player.closeInventory();
|
||||
} else {
|
||||
openMain(player, 0);
|
||||
}
|
||||
} else {
|
||||
messages.play(player, "gui.error-sound");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleAdminMainClick(Player player, int slot) {
|
||||
if (slot == 20) {
|
||||
openMain(player, 0);
|
||||
} else if (slot == 22) {
|
||||
openAdminHistory(player, 0);
|
||||
} else if (slot == 24) {
|
||||
openAdminSuspicious(player, 0);
|
||||
} else if (slot == slot("layout.back-slot", 46)) {
|
||||
openMain(player, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePagedAdminClick(Player player, GuiHolder holder, int slot, GuiType type) {
|
||||
if (slot == slot("layout.back-slot", 46)) {
|
||||
openAdminMain(player);
|
||||
return;
|
||||
}
|
||||
if (slot == slot("layout.previous-slot", 45) && holder.page() > 0) {
|
||||
if (type == GuiType.ADMIN_HISTORY) {
|
||||
openAdminHistory(player, holder.page() - 1);
|
||||
} else {
|
||||
openAdminSuspicious(player, holder.page() - 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (slot == slot("layout.next-slot", 53)) {
|
||||
if (type == GuiType.ADMIN_HISTORY) {
|
||||
openAdminHistory(player, holder.page() + 1);
|
||||
} else {
|
||||
openAdminSuspicious(player, holder.page() + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void fillBounties(Inventory inventory, List<Bounty> bounties, int page, boolean top) {
|
||||
List<Integer> slots = contentSlots();
|
||||
int start = page * slots.size();
|
||||
if (bounties.isEmpty()) {
|
||||
inventory.setItem(22, item("items.empty", Map.of(), null));
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < slots.size() && start + i < bounties.size(); i++) {
|
||||
Bounty bounty = bounties.get(start + i);
|
||||
Map<String, String> placeholders = bountyService.bountyPlaceholders(bounty);
|
||||
placeholders.put("rank", String.valueOf(start + i + 1));
|
||||
ConfigurationSection section = configService.guiSection(top ? "items.top-bounty" : "items.bounty");
|
||||
inventory.setItem(slots.get(i), ItemBuilder.fromSection(section, messages, placeholders, Bukkit.getOfflinePlayer(bounty.targetUuid())));
|
||||
}
|
||||
}
|
||||
|
||||
private Bounty bountyAt(int page, int slot) {
|
||||
List<Integer> slots = contentSlots();
|
||||
int index = slots.indexOf(slot);
|
||||
if (index < 0) {
|
||||
return null;
|
||||
}
|
||||
int global = page * slots.size() + index;
|
||||
List<Bounty> bounties = bountyService.activeSorted();
|
||||
return global >= 0 && global < bounties.size() ? bounties.get(global) : null;
|
||||
}
|
||||
|
||||
private void addMainButtons(Inventory inventory, int page, int totalItems, GuiType type, Player player) {
|
||||
addPageButtons(inventory, page, totalItems, type);
|
||||
inventory.setItem(slot("layout.place-slot", 48), item("items.place", Map.of(), null));
|
||||
inventory.setItem(slot("layout.top-slot", 50), item(type == GuiType.TOP ? "items.back" : "items.top", Map.of(), null));
|
||||
inventory.setItem(slot("layout.refresh-slot", 49), item("items.refresh", Map.of(), null));
|
||||
if (player.hasPermission("dirtbounties.admin")) {
|
||||
inventory.setItem(slot("layout.admin-slot", 52), item("items.admin-active", Map.of(), null));
|
||||
}
|
||||
}
|
||||
|
||||
private void addPageButtons(Inventory inventory, int page, int totalItems, GuiType type) {
|
||||
List<Integer> slots = contentSlots();
|
||||
if (page > 0) {
|
||||
inventory.setItem(slot("layout.previous-slot", 45), item("items.previous", Map.of(), null));
|
||||
}
|
||||
if ((page + 1) * slots.size() < totalItems) {
|
||||
inventory.setItem(slot("layout.next-slot", 53), item("items.next", Map.of(), null));
|
||||
}
|
||||
if (type == GuiType.ADMIN_HISTORY || type == GuiType.ADMIN_SUSPICIOUS) {
|
||||
inventory.setItem(slot("layout.refresh-slot", 49), item("items.refresh", Map.of(), null));
|
||||
}
|
||||
}
|
||||
|
||||
private Inventory create(GuiType type, int page, UUID targetUuid, String titlePath, Map<String, String> placeholders) {
|
||||
int size = Math.max(9, configService.gui().getInt("layout.size", 54));
|
||||
size = Math.min(54, ((size + 8) / 9) * 9);
|
||||
GuiHolder holder = new GuiHolder(type, Math.max(0, page), targetUuid);
|
||||
String title = configService.gui().getString(titlePath, "DirtBounties");
|
||||
Inventory inventory = Bukkit.createInventory(holder, size, messages.legacy(title, placeholders));
|
||||
holder.inventory(inventory);
|
||||
return inventory;
|
||||
}
|
||||
|
||||
private void decorate(Inventory inventory) {
|
||||
ItemStack filler = item("items.filler", Map.of(), null);
|
||||
ItemStack border = item("items.border", Map.of(), null);
|
||||
for (int i = 0; i < inventory.getSize(); i++) {
|
||||
inventory.setItem(i, filler);
|
||||
}
|
||||
for (int i = 0; i < inventory.getSize(); i++) {
|
||||
boolean isBorder = i < 9 || i >= inventory.getSize() - 9 || i % 9 == 0 || i % 9 == 8;
|
||||
if (isBorder) {
|
||||
inventory.setItem(i, border);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ItemStack item(String path, Map<String, String> placeholders, OfflinePlayer owner) {
|
||||
return ItemBuilder.fromSection(configService.guiSection(path), messages, placeholders, owner);
|
||||
}
|
||||
|
||||
private ItemStack anonymousToggle(PendingBountyInput input) {
|
||||
String state = input.anonymous() ? "&aEnabled" : "&cDisabled";
|
||||
return ItemBuilder.simple(Material.NAME_TAG, "&6Anonymous", List.of("&7Current: " + state, "", "&eClick to toggle."),
|
||||
messages, Map.of());
|
||||
}
|
||||
|
||||
private Map<String, String> confirmPlaceholders(PendingBountyInput input) {
|
||||
BountyService.Fee fee = bountyService.previewFee(input.amount(), input.increase());
|
||||
Map<String, String> placeholders = new HashMap<>();
|
||||
placeholders.put("target", input.targetName() == null ? "Unknown" : input.targetName());
|
||||
placeholders.put("amount", bountyService.formatAmount(fee.bountyAmount()));
|
||||
placeholders.put("fee", bountyService.formatAmount(fee.feeAmount()));
|
||||
placeholders.put("cost", bountyService.formatAmount(fee.totalCost()));
|
||||
placeholders.put("reason", input.reason() == null || input.reason().isBlank()
|
||||
? configService.main().getString("bounties.default-reason", "No reason given.")
|
||||
: input.reason());
|
||||
return placeholders;
|
||||
}
|
||||
|
||||
private Map<String, String> claimRulePlaceholders() {
|
||||
Map<String, String> placeholders = new HashMap<>();
|
||||
placeholders.put("pvp", bool(configService.main().getBoolean("claim-rules.require-pvp-kill", true)));
|
||||
placeholders.put("same_ip", bool(configService.main().getBoolean("claim-rules.prevent-same-ip-claims", true)));
|
||||
placeholders.put("world_mode", configService.main().getString("claim-rules.worlds.mode", "blacklist"));
|
||||
placeholders.put("combat", bool(configService.main().getBoolean("claim-rules.combat.enabled", true)));
|
||||
return placeholders;
|
||||
}
|
||||
|
||||
private String reasonLines(Bounty bounty) {
|
||||
List<String> lines = new ArrayList<>();
|
||||
List<BountyContribution> contributions = bounty.contributions();
|
||||
int start = Math.max(0, contributions.size() - 5);
|
||||
for (int i = contributions.size() - 1; i >= start; i--) {
|
||||
BountyContribution contribution = contributions.get(i);
|
||||
String placer = contribution.anonymous() ? "Anonymous" : contribution.placerName();
|
||||
lines.add("&8- &7" + placer + ": &f" + contribution.reason());
|
||||
}
|
||||
if (lines.isEmpty()) {
|
||||
lines.add("&8- &7No contribution notes.");
|
||||
}
|
||||
return String.join("\n", lines);
|
||||
}
|
||||
|
||||
private List<Integer> contentSlots() {
|
||||
List<Integer> slots = configService.gui().getIntegerList("layout.content-slots");
|
||||
if (!slots.isEmpty()) {
|
||||
return slots;
|
||||
}
|
||||
return List.of(10, 11, 12, 13, 14, 15, 16, 19, 20, 21, 22, 23, 24, 25, 28, 29, 30, 31, 32, 33, 34, 37, 38, 39, 40, 41, 42, 43);
|
||||
}
|
||||
|
||||
private int slot(String path, int fallback) {
|
||||
return configService.gui().getInt(path, fallback);
|
||||
}
|
||||
|
||||
private boolean enabled(Player player) {
|
||||
if (!configService.main().getBoolean("gui.enabled", true)) {
|
||||
messages.send(player, "gui.unavailable");
|
||||
return false;
|
||||
}
|
||||
if (!player.hasPermission("dirtbounties.use")) {
|
||||
messages.send(player, "no-permission");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private String bool(boolean value) {
|
||||
return value ? "yes" : "no";
|
||||
}
|
||||
|
||||
private long inputExpiresAt() {
|
||||
long timeout = TimeUtil.parseMillis(configService.main().getString("gui.chat-input-timeout"), 60_000L);
|
||||
return System.currentTimeMillis() + Math.max(5_000L, timeout);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.dirtbagmc.dirtbounties.gui;
|
||||
|
||||
public enum GuiType {
|
||||
MAIN,
|
||||
TOP,
|
||||
DETAIL,
|
||||
CONFIRM,
|
||||
ADMIN_MAIN,
|
||||
ADMIN_HISTORY,
|
||||
ADMIN_SUSPICIOUS
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.dirtbagmc.dirtbounties.gui;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public final class PendingBountyInput {
|
||||
private final UUID playerUuid;
|
||||
private UUID targetUuid;
|
||||
private String targetName;
|
||||
private double amount;
|
||||
private String reason = "";
|
||||
private boolean anonymous;
|
||||
private boolean increase;
|
||||
private Stage stage;
|
||||
private long expiresAt;
|
||||
|
||||
public PendingBountyInput(UUID playerUuid, Stage stage, long expiresAt) {
|
||||
this.playerUuid = playerUuid;
|
||||
this.stage = stage;
|
||||
this.expiresAt = expiresAt;
|
||||
}
|
||||
|
||||
public UUID playerUuid() {
|
||||
return playerUuid;
|
||||
}
|
||||
|
||||
public UUID targetUuid() {
|
||||
return targetUuid;
|
||||
}
|
||||
|
||||
public void targetUuid(UUID targetUuid) {
|
||||
this.targetUuid = targetUuid;
|
||||
}
|
||||
|
||||
public String targetName() {
|
||||
return targetName;
|
||||
}
|
||||
|
||||
public void targetName(String targetName) {
|
||||
this.targetName = targetName;
|
||||
}
|
||||
|
||||
public double amount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public void amount(double amount) {
|
||||
this.amount = amount;
|
||||
}
|
||||
|
||||
public String reason() {
|
||||
return reason;
|
||||
}
|
||||
|
||||
public void reason(String reason) {
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
public boolean anonymous() {
|
||||
return anonymous;
|
||||
}
|
||||
|
||||
public void anonymous(boolean anonymous) {
|
||||
this.anonymous = anonymous;
|
||||
}
|
||||
|
||||
public boolean increase() {
|
||||
return increase;
|
||||
}
|
||||
|
||||
public void increase(boolean increase) {
|
||||
this.increase = increase;
|
||||
}
|
||||
|
||||
public Stage stage() {
|
||||
return stage;
|
||||
}
|
||||
|
||||
public void stage(Stage stage) {
|
||||
this.stage = stage;
|
||||
}
|
||||
|
||||
public long expiresAt() {
|
||||
return expiresAt;
|
||||
}
|
||||
|
||||
public void expiresAt(long expiresAt) {
|
||||
this.expiresAt = expiresAt;
|
||||
}
|
||||
|
||||
public enum Stage {
|
||||
TARGET,
|
||||
AMOUNT,
|
||||
REASON,
|
||||
CONFIRM
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.dirtbagmc.dirtbounties.hook;
|
||||
|
||||
import com.dirtbagmc.dirtbounties.model.Bounty;
|
||||
import com.dirtbagmc.dirtbounties.service.BountyService;
|
||||
import com.dirtbagmc.dirtbounties.service.HistoryService;
|
||||
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
public final class DirtBountiesExpansion extends PlaceholderExpansion {
|
||||
private final BountyService bountyService;
|
||||
private final HistoryService historyService;
|
||||
private final String version;
|
||||
|
||||
public DirtBountiesExpansion(BountyService bountyService, HistoryService historyService, String version) {
|
||||
this.bountyService = bountyService;
|
||||
this.historyService = historyService;
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String getIdentifier() {
|
||||
return "dirtbounties";
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String getAuthor() {
|
||||
return "DirtbagMC";
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean persist() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String onRequest(OfflinePlayer player, @NotNull String params) {
|
||||
if (params.equalsIgnoreCase("total_active_bounties")) {
|
||||
return String.valueOf(bountyService.activeBounties().size());
|
||||
}
|
||||
if (params.equalsIgnoreCase("total_active_value")) {
|
||||
return bountyService.formatAmount(bountyService.totalActiveValue());
|
||||
}
|
||||
if (params.equalsIgnoreCase("top_target")) {
|
||||
return bountyService.activeSorted().stream().findFirst().map(Bounty::targetName).orElse("");
|
||||
}
|
||||
if (params.equalsIgnoreCase("top_amount")) {
|
||||
return bountyService.activeSorted().stream().findFirst().map(bounty -> bountyService.formatAmount(bounty.amount())).orElse("0");
|
||||
}
|
||||
if (player != null) {
|
||||
if (params.equalsIgnoreCase("current_player_bounty") || params.equalsIgnoreCase("player_bounty")) {
|
||||
return bountyService.bounty(player.getUniqueId())
|
||||
.map(bounty -> bountyService.formatAmount(bounty.amount()))
|
||||
.orElse("0");
|
||||
}
|
||||
if (params.equalsIgnoreCase("has_bounty")) {
|
||||
return bountyService.bounty(player.getUniqueId()).isPresent() ? "yes" : "no";
|
||||
}
|
||||
if (params.equalsIgnoreCase("claimed_count")) {
|
||||
return String.valueOf(historyService.claimedCountBy(player.getUniqueId()));
|
||||
}
|
||||
}
|
||||
if (params.startsWith("rank_")) {
|
||||
int rank;
|
||||
try {
|
||||
rank = Integer.parseInt(params.substring("rank_".length()));
|
||||
} catch (NumberFormatException ex) {
|
||||
return "";
|
||||
}
|
||||
if (rank <= 0) {
|
||||
return "";
|
||||
}
|
||||
return bountyService.activeSorted().stream()
|
||||
.sorted(Comparator.comparingDouble(Bounty::amount).reversed())
|
||||
.skip(rank - 1L)
|
||||
.findFirst()
|
||||
.map(bounty -> bounty.targetName() + ":" + bountyService.formatAmount(bounty.amount()))
|
||||
.orElse("");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String onPlaceholderRequest(Player player, @NotNull String params) {
|
||||
return onRequest(player, params);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.dirtbagmc.dirtbounties.listener;
|
||||
|
||||
import com.dirtbagmc.dirtbounties.gui.GuiManager;
|
||||
import com.dirtbagmc.dirtbounties.service.BountyService;
|
||||
import com.dirtbagmc.dirtbounties.service.CombatTracker;
|
||||
import com.dirtbagmc.dirtbounties.service.PlayerCacheService;
|
||||
import io.papermc.paper.event.player.AsyncChatEvent;
|
||||
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.entity.Projectile;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.entity.EntityDamageByEntityEvent;
|
||||
import org.bukkit.event.entity.PlayerDeathEvent;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.bukkit.projectiles.ProjectileSource;
|
||||
|
||||
public final class BountyListener implements Listener {
|
||||
private final JavaPlugin plugin;
|
||||
private final GuiManager guiManager;
|
||||
private final BountyService bountyService;
|
||||
private final PlayerCacheService playerCacheService;
|
||||
private final CombatTracker combatTracker;
|
||||
|
||||
public BountyListener(JavaPlugin plugin, GuiManager guiManager, BountyService bountyService,
|
||||
PlayerCacheService playerCacheService, CombatTracker combatTracker) {
|
||||
this.plugin = plugin;
|
||||
this.guiManager = guiManager;
|
||||
this.bountyService = bountyService;
|
||||
this.playerCacheService = playerCacheService;
|
||||
this.combatTracker = combatTracker;
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onJoin(PlayerJoinEvent event) {
|
||||
playerCacheService.update(event.getPlayer());
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onDamage(EntityDamageByEntityEvent event) {
|
||||
if (!(event.getEntity() instanceof Player victim)) {
|
||||
return;
|
||||
}
|
||||
Player attacker = attacker(event.getDamager());
|
||||
if (attacker == null) {
|
||||
return;
|
||||
}
|
||||
combatTracker.record(attacker, victim, event.getFinalDamage());
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onDeath(PlayerDeathEvent event) {
|
||||
bountyService.handleDeath(event.getEntity(), event.getEntity().getKiller());
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void onInventoryClick(InventoryClickEvent event) {
|
||||
guiManager.handleClick(event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void onChat(AsyncChatEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
if (!guiManager.hasPendingInput(player.getUniqueId())) {
|
||||
return;
|
||||
}
|
||||
String plain = PlainTextComponentSerializer.plainText().serialize(event.message()).trim();
|
||||
event.setCancelled(true);
|
||||
Bukkit.getScheduler().runTask(plugin, () -> guiManager.acceptChatInput(player, plain));
|
||||
}
|
||||
|
||||
private Player attacker(Entity damager) {
|
||||
if (damager instanceof Player player) {
|
||||
return player;
|
||||
}
|
||||
if (damager instanceof Projectile projectile) {
|
||||
ProjectileSource shooter = projectile.getShooter();
|
||||
if (shooter instanceof Player player) {
|
||||
return player;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.dirtbagmc.dirtbounties.model;
|
||||
|
||||
import com.dirtbagmc.dirtbounties.util.NumberUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class Bounty {
|
||||
private final UUID id;
|
||||
private final UUID targetUuid;
|
||||
private String targetName;
|
||||
private double amount;
|
||||
private final long createdAt;
|
||||
private long updatedAt;
|
||||
private long expiresAt;
|
||||
private final List<BountyContribution> contributions;
|
||||
|
||||
public Bounty(UUID id, UUID targetUuid, String targetName, double amount, long createdAt,
|
||||
long updatedAt, long expiresAt, List<BountyContribution> contributions) {
|
||||
this.id = id;
|
||||
this.targetUuid = targetUuid;
|
||||
this.targetName = targetName;
|
||||
this.amount = NumberUtil.clampMoney(amount);
|
||||
this.createdAt = createdAt;
|
||||
this.updatedAt = updatedAt;
|
||||
this.expiresAt = expiresAt;
|
||||
this.contributions = new ArrayList<>(contributions == null ? List.of() : contributions);
|
||||
}
|
||||
|
||||
public UUID id() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public UUID targetUuid() {
|
||||
return targetUuid;
|
||||
}
|
||||
|
||||
public String targetName() {
|
||||
return targetName;
|
||||
}
|
||||
|
||||
public void targetName(String targetName) {
|
||||
this.targetName = targetName;
|
||||
}
|
||||
|
||||
public double amount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public long createdAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public long updatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public long expiresAt() {
|
||||
return expiresAt;
|
||||
}
|
||||
|
||||
public void expiresAt(long expiresAt) {
|
||||
this.expiresAt = expiresAt;
|
||||
}
|
||||
|
||||
public List<BountyContribution> contributions() {
|
||||
return Collections.unmodifiableList(contributions);
|
||||
}
|
||||
|
||||
public void addContribution(BountyContribution contribution) {
|
||||
contributions.add(contribution);
|
||||
amount = NumberUtil.clampMoney(amount + contribution.amount());
|
||||
updatedAt = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public void setAmount(double amount) {
|
||||
this.amount = NumberUtil.clampMoney(amount);
|
||||
this.updatedAt = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public boolean isExpired(long now) {
|
||||
return expiresAt > 0L && now >= expiresAt;
|
||||
}
|
||||
|
||||
public String topReason(String fallback) {
|
||||
for (int i = contributions.size() - 1; i >= 0; i--) {
|
||||
String reason = contributions.get(i).reason();
|
||||
if (reason != null && !reason.isBlank()) {
|
||||
return reason;
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.dirtbagmc.dirtbounties.model;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public final class BountyContribution {
|
||||
private final UUID id;
|
||||
private final UUID placerUuid;
|
||||
private final String placerName;
|
||||
private final double amount;
|
||||
private final double fee;
|
||||
private final String reason;
|
||||
private final boolean anonymous;
|
||||
private final long createdAt;
|
||||
|
||||
public BountyContribution(UUID id, UUID placerUuid, String placerName, double amount, double fee,
|
||||
String reason, boolean anonymous, long createdAt) {
|
||||
this.id = id;
|
||||
this.placerUuid = placerUuid;
|
||||
this.placerName = placerName;
|
||||
this.amount = amount;
|
||||
this.fee = fee;
|
||||
this.reason = reason;
|
||||
this.anonymous = anonymous;
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public UUID id() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public UUID placerUuid() {
|
||||
return placerUuid;
|
||||
}
|
||||
|
||||
public String placerName() {
|
||||
return placerName;
|
||||
}
|
||||
|
||||
public double amount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public double fee() {
|
||||
return fee;
|
||||
}
|
||||
|
||||
public String reason() {
|
||||
return reason;
|
||||
}
|
||||
|
||||
public boolean anonymous() {
|
||||
return anonymous;
|
||||
}
|
||||
|
||||
public long createdAt() {
|
||||
return createdAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.dirtbagmc.dirtbounties.model;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public final class BountyHistoryRecord {
|
||||
private final UUID id;
|
||||
private final String type;
|
||||
private final UUID targetUuid;
|
||||
private final String targetName;
|
||||
private final UUID actorUuid;
|
||||
private final String actorName;
|
||||
private final double amount;
|
||||
private final String note;
|
||||
private final long createdAt;
|
||||
|
||||
public BountyHistoryRecord(UUID id, String type, UUID targetUuid, String targetName, UUID actorUuid,
|
||||
String actorName, double amount, String note, long createdAt) {
|
||||
this.id = id;
|
||||
this.type = type;
|
||||
this.targetUuid = targetUuid;
|
||||
this.targetName = targetName;
|
||||
this.actorUuid = actorUuid;
|
||||
this.actorName = actorName;
|
||||
this.amount = amount;
|
||||
this.note = note;
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public UUID id() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String type() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public UUID targetUuid() {
|
||||
return targetUuid;
|
||||
}
|
||||
|
||||
public String targetName() {
|
||||
return targetName;
|
||||
}
|
||||
|
||||
public UUID actorUuid() {
|
||||
return actorUuid;
|
||||
}
|
||||
|
||||
public String actorName() {
|
||||
return actorName;
|
||||
}
|
||||
|
||||
public double amount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public String note() {
|
||||
return note;
|
||||
}
|
||||
|
||||
public long createdAt() {
|
||||
return createdAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.dirtbagmc.dirtbounties.model;
|
||||
|
||||
public record ClaimValidation(boolean allowed, String messageKey, String reason) {
|
||||
public static ClaimValidation allow() {
|
||||
return new ClaimValidation(true, "", "");
|
||||
}
|
||||
|
||||
public static ClaimValidation denied(String messageKey, String reason) {
|
||||
return new ClaimValidation(false, messageKey, reason);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.dirtbagmc.dirtbounties.model;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public final class SuspiciousActivity {
|
||||
private final UUID id;
|
||||
private final String type;
|
||||
private final UUID killerUuid;
|
||||
private final String killerName;
|
||||
private final UUID targetUuid;
|
||||
private final String targetName;
|
||||
private final String details;
|
||||
private final int severity;
|
||||
private final long createdAt;
|
||||
|
||||
public SuspiciousActivity(UUID id, String type, UUID killerUuid, String killerName, UUID targetUuid,
|
||||
String targetName, String details, int severity, long createdAt) {
|
||||
this.id = id;
|
||||
this.type = type;
|
||||
this.killerUuid = killerUuid;
|
||||
this.killerName = killerName;
|
||||
this.targetUuid = targetUuid;
|
||||
this.targetName = targetName;
|
||||
this.details = details;
|
||||
this.severity = severity;
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public UUID id() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String type() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public UUID killerUuid() {
|
||||
return killerUuid;
|
||||
}
|
||||
|
||||
public String killerName() {
|
||||
return killerName;
|
||||
}
|
||||
|
||||
public UUID targetUuid() {
|
||||
return targetUuid;
|
||||
}
|
||||
|
||||
public String targetName() {
|
||||
return targetName;
|
||||
}
|
||||
|
||||
public String details() {
|
||||
return details;
|
||||
}
|
||||
|
||||
public int severity() {
|
||||
return severity;
|
||||
}
|
||||
|
||||
public long createdAt() {
|
||||
return createdAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.dirtbagmc.dirtbounties.service;
|
||||
|
||||
import com.dirtbagmc.dirtbounties.config.ConfigService;
|
||||
import com.dirtbagmc.dirtbounties.model.ClaimValidation;
|
||||
import com.dirtbagmc.dirtbounties.util.TimeUtil;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class AntiAbuseService {
|
||||
private final ConfigService configService;
|
||||
private final HistoryService historyService;
|
||||
private final Map<UUID, Long> runtimeKillerClaims = new HashMap<>();
|
||||
private final Map<UUID, Long> runtimeTargetClaims = new HashMap<>();
|
||||
private final Map<String, Long> runtimePairClaims = new HashMap<>();
|
||||
|
||||
public AntiAbuseService(ConfigService configService, HistoryService historyService) {
|
||||
this.configService = configService;
|
||||
this.historyService = historyService;
|
||||
}
|
||||
|
||||
public ClaimValidation validate(Player killer, Player target) {
|
||||
if (!configService.main().getBoolean("anti-abuse.enabled", true)
|
||||
|| killer.hasPermission("dirtbounties.bypass.cooldowns")) {
|
||||
return ClaimValidation.allow();
|
||||
}
|
||||
long now = System.currentTimeMillis();
|
||||
long killerCooldown = TimeUtil.parseMillis(configService.main().getString("anti-abuse.killer-claim-cooldown"), 120_000L);
|
||||
long targetCooldown = TimeUtil.parseMillis(configService.main().getString("anti-abuse.target-claim-cooldown"), 120_000L);
|
||||
long pairCooldown = TimeUtil.parseMillis(configService.main().getString("anti-abuse.killer-target-pair-cooldown"), 43_200_000L);
|
||||
|
||||
long lastKiller = Math.max(runtimeKillerClaims.getOrDefault(killer.getUniqueId(), 0L),
|
||||
historyService.lastClaimByKiller(killer.getUniqueId()));
|
||||
if (killerCooldown > 0L && now - lastKiller < killerCooldown) {
|
||||
return ClaimValidation.denied("claim-denied.cooldown", "Killer claim cooldown");
|
||||
}
|
||||
|
||||
long lastTarget = Math.max(runtimeTargetClaims.getOrDefault(target.getUniqueId(), 0L),
|
||||
historyService.lastClaimOnTarget(target.getUniqueId()));
|
||||
if (targetCooldown > 0L && now - lastTarget < targetCooldown) {
|
||||
return ClaimValidation.denied("claim-denied.cooldown", "Target claim cooldown");
|
||||
}
|
||||
|
||||
String pair = pair(killer.getUniqueId(), target.getUniqueId());
|
||||
long lastPair = Math.max(runtimePairClaims.getOrDefault(pair, 0L),
|
||||
historyService.lastPairClaim(killer.getUniqueId(), target.getUniqueId()));
|
||||
if (pairCooldown > 0L && now - lastPair < pairCooldown) {
|
||||
return ClaimValidation.denied("claim-denied.cooldown", "Killer-target pair cooldown");
|
||||
}
|
||||
|
||||
long window = TimeUtil.parseMillis(configService.main().getString("anti-abuse.pair-window"), 604_800_000L);
|
||||
int max = configService.main().getInt("anti-abuse.max-pair-claims-in-window", 2);
|
||||
if (max > 0 && historyService.claimCount(killer.getUniqueId(), target.getUniqueId(), now - window) >= max) {
|
||||
return ClaimValidation.denied("claim-denied.pair-limit", "Killer-target pair claim limit");
|
||||
}
|
||||
|
||||
return ClaimValidation.allow();
|
||||
}
|
||||
|
||||
public void recordClaim(Player killer, Player target) {
|
||||
long now = System.currentTimeMillis();
|
||||
runtimeKillerClaims.put(killer.getUniqueId(), now);
|
||||
runtimeTargetClaims.put(target.getUniqueId(), now);
|
||||
runtimePairClaims.put(pair(killer.getUniqueId(), target.getUniqueId()), now);
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
runtimeKillerClaims.clear();
|
||||
runtimeTargetClaims.clear();
|
||||
runtimePairClaims.clear();
|
||||
}
|
||||
|
||||
private String pair(UUID killer, UUID target) {
|
||||
return killer + ":" + target;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,641 @@
|
||||
package com.dirtbagmc.dirtbounties.service;
|
||||
|
||||
import com.dirtbagmc.dirtbounties.config.ConfigService;
|
||||
import com.dirtbagmc.dirtbounties.economy.EconomyService;
|
||||
import com.dirtbagmc.dirtbounties.model.Bounty;
|
||||
import com.dirtbagmc.dirtbounties.model.BountyContribution;
|
||||
import com.dirtbagmc.dirtbounties.model.ClaimValidation;
|
||||
import com.dirtbagmc.dirtbounties.storage.StorageManager;
|
||||
import com.dirtbagmc.dirtbounties.util.NumberUtil;
|
||||
import com.dirtbagmc.dirtbounties.util.TimeUtil;
|
||||
import com.dirtbagmc.dirtbounties.webhook.WebhookService;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public final class BountyService {
|
||||
private static final Pattern VALID_PLAYER_NAME = Pattern.compile("^[A-Za-z0-9_]{3,16}$");
|
||||
|
||||
private final JavaPlugin plugin;
|
||||
private final ConfigService configService;
|
||||
private final MessageService messages;
|
||||
private final EconomyService economy;
|
||||
private final StorageManager storage;
|
||||
private final HistoryService history;
|
||||
private final PlayerCacheService playerCache;
|
||||
private final CombatTracker combatTracker;
|
||||
private final AntiAbuseService antiAbuse;
|
||||
private final WebhookService webhooks;
|
||||
private final Map<UUID, Bounty> bounties = new HashMap<>();
|
||||
private final Map<UUID, Long> placementCooldowns = new HashMap<>();
|
||||
|
||||
public BountyService(JavaPlugin plugin, ConfigService configService, MessageService messages, EconomyService economy,
|
||||
StorageManager storage, HistoryService history, PlayerCacheService playerCache,
|
||||
CombatTracker combatTracker, AntiAbuseService antiAbuse, WebhookService webhooks) {
|
||||
this.plugin = plugin;
|
||||
this.configService = configService;
|
||||
this.messages = messages;
|
||||
this.economy = economy;
|
||||
this.storage = storage;
|
||||
this.history = history;
|
||||
this.playerCache = playerCache;
|
||||
this.combatTracker = combatTracker;
|
||||
this.antiAbuse = antiAbuse;
|
||||
this.webhooks = webhooks;
|
||||
}
|
||||
|
||||
public void load() {
|
||||
bounties.clear();
|
||||
bounties.putAll(storage.loadBounties());
|
||||
}
|
||||
|
||||
public void save() {
|
||||
storage.saveBounties(bounties.values());
|
||||
}
|
||||
|
||||
public void clearRuntimeState() {
|
||||
placementCooldowns.clear();
|
||||
bounties.clear();
|
||||
}
|
||||
|
||||
public Optional<Bounty> bounty(UUID targetUuid) {
|
||||
return Optional.ofNullable(bounties.get(targetUuid));
|
||||
}
|
||||
|
||||
public Collection<Bounty> activeBounties() {
|
||||
return List.copyOf(bounties.values());
|
||||
}
|
||||
|
||||
public List<Bounty> activeSorted() {
|
||||
return bounties.values().stream()
|
||||
.sorted(Comparator.comparingDouble(Bounty::amount).reversed()
|
||||
.thenComparing(Bounty::updatedAt, Comparator.reverseOrder()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
public List<Bounty> topBounties(int limit) {
|
||||
return activeSorted().stream().limit(limit).toList();
|
||||
}
|
||||
|
||||
public double totalActiveValue() {
|
||||
return bounties.values().stream().mapToDouble(Bounty::amount).sum();
|
||||
}
|
||||
|
||||
public OfflinePlayer resolveTarget(String name) {
|
||||
if (name == null || !VALID_PLAYER_NAME.matcher(name).matches()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Player exact = Bukkit.getPlayerExact(name);
|
||||
if (exact != null) {
|
||||
return exact;
|
||||
}
|
||||
for (Player online : Bukkit.getOnlinePlayers()) {
|
||||
if (online.getName().equalsIgnoreCase(name)) {
|
||||
return online;
|
||||
}
|
||||
}
|
||||
for (OfflinePlayer offline : Bukkit.getOfflinePlayers()) {
|
||||
if (offline.getName() != null && offline.getName().equalsIgnoreCase(name)) {
|
||||
return offline;
|
||||
}
|
||||
}
|
||||
if (configService.main().getBoolean("bounties.allow-never-joined-targets", false)) {
|
||||
return Bukkit.getOfflinePlayer(name);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean placeBounty(Player placer, OfflinePlayer target, double requestedAmount, String reason,
|
||||
boolean anonymous, boolean increaseOnly) {
|
||||
String permission = increaseOnly ? "dirtbounties.add" : "dirtbounties.place";
|
||||
if (!placer.hasPermission(permission)) {
|
||||
messages.send(placer, "no-permission");
|
||||
return false;
|
||||
}
|
||||
if (!validateTargetForPlacement(placer, target)) {
|
||||
return false;
|
||||
}
|
||||
if (!economy.isReady()) {
|
||||
messages.send(placer, "economy-missing");
|
||||
return false;
|
||||
}
|
||||
|
||||
requestedAmount = NumberUtil.clampMoney(requestedAmount);
|
||||
double min = configService.main().getDouble("bounties.min-amount", 100.0);
|
||||
double max = configService.main().getDouble("bounties.max-amount", 1_000_000.0);
|
||||
if (requestedAmount < min) {
|
||||
messages.send(placer, "amount-too-low", Map.of("min", economy.format(min)));
|
||||
return false;
|
||||
}
|
||||
if (requestedAmount > max) {
|
||||
messages.send(placer, "amount-too-high", Map.of("max", economy.format(max)));
|
||||
return false;
|
||||
}
|
||||
|
||||
long cooldown = TimeUtil.parseMillis(configService.main().getString("bounties.placement-cooldown"), 30_000L);
|
||||
long lastPlacement = placementCooldowns.getOrDefault(placer.getUniqueId(), 0L);
|
||||
if (cooldown > 0L && !placer.hasPermission("dirtbounties.bypass.cooldowns")
|
||||
&& System.currentTimeMillis() - lastPlacement < cooldown) {
|
||||
messages.send(placer, "cooldown", Map.of("time", TimeUtil.formatDuration(cooldown - (System.currentTimeMillis() - lastPlacement))));
|
||||
return false;
|
||||
}
|
||||
|
||||
reason = cleanReason(placer, reason);
|
||||
if (reason == null) {
|
||||
return false;
|
||||
}
|
||||
anonymous = sanitizeAnonymous(placer, anonymous);
|
||||
|
||||
Bounty existing = bounties.get(target.getUniqueId());
|
||||
if (increaseOnly && existing == null) {
|
||||
messages.send(placer, "bounty-not-found", Map.of("target", safeName(target)));
|
||||
return false;
|
||||
}
|
||||
if (existing != null && !configService.main().getBoolean("bounties.stack-existing", true)) {
|
||||
messages.send(placer, "amount-would-exceed-max", Map.of("max", economy.format(max)));
|
||||
return false;
|
||||
}
|
||||
|
||||
Fee fee = calculateFee(requestedAmount, increaseOnly);
|
||||
double previousTotal = existing == null ? 0.0 : existing.amount();
|
||||
if (previousTotal + fee.bountyAmount() > max) {
|
||||
messages.send(placer, "amount-would-exceed-max", Map.of("max", economy.format(max)));
|
||||
return false;
|
||||
}
|
||||
|
||||
double minimumBalanceAfter = configService.main().getDouble("economy.minimum-balance-after-withdraw", 0.0);
|
||||
if (economy.balance(placer) - fee.totalCost() < minimumBalanceAfter) {
|
||||
messages.send(placer, "not-enough-money", Map.of("cost", economy.format(fee.totalCost())));
|
||||
return false;
|
||||
}
|
||||
EconomyService.EconomyResult withdraw = economy.withdraw(placer, fee.totalCost());
|
||||
if (!withdraw.success()) {
|
||||
messages.send(placer, "not-enough-money", Map.of("cost", economy.format(fee.totalCost())));
|
||||
return false;
|
||||
}
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
String targetName = safeName(target);
|
||||
Bounty bounty = existing == null
|
||||
? new Bounty(UUID.randomUUID(), target.getUniqueId(), targetName, 0.0, now, now, defaultExpiresAt(now), List.of())
|
||||
: existing;
|
||||
bounty.targetName(targetName);
|
||||
bounty.addContribution(new BountyContribution(UUID.randomUUID(), placer.getUniqueId(), placer.getName(),
|
||||
fee.bountyAmount(), fee.feeAmount(), reason, anonymous, now));
|
||||
bounties.put(target.getUniqueId(), bounty);
|
||||
placementCooldowns.put(placer.getUniqueId(), now);
|
||||
save();
|
||||
|
||||
String type = existing == null ? "PLACED" : "INCREASED";
|
||||
history.record(type, target.getUniqueId(), targetName, placer.getUniqueId(), placer.getName(),
|
||||
fee.bountyAmount(), reason + (anonymous ? " (anonymous)" : ""));
|
||||
|
||||
Map<String, String> placeholders = bountyPlaceholders(bounty);
|
||||
placeholders.put("placer", anonymous ? "Anonymous" : placer.getName());
|
||||
placeholders.put("amount", economy.format(fee.bountyAmount()));
|
||||
placeholders.put("fee", economy.format(fee.feeAmount()));
|
||||
placeholders.put("cost", economy.format(fee.totalCost()));
|
||||
|
||||
messages.send(placer, existing == null ? "bounty.placed" : "bounty.added", placeholders);
|
||||
maybeBroadcastPlacement(existing == null, previousTotal, bounty, placeholders);
|
||||
if (configService.main().getBoolean("logging.console.placements", true)) {
|
||||
plugin.getLogger().info(placer.getName() + " placed/increased bounty on " + targetName + " for " + fee.bountyAmount());
|
||||
}
|
||||
webhooks.placement(placer.getName(), targetName, bounty.amount(), reason, existing != null);
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean adminSet(CommandSender sender, OfflinePlayer target, double amount) {
|
||||
if (target == null) {
|
||||
messages.send(sender, "invalid-player");
|
||||
return false;
|
||||
}
|
||||
amount = NumberUtil.clampMoney(amount);
|
||||
if (amount <= 0.0) {
|
||||
messages.send(sender, "invalid-number");
|
||||
return false;
|
||||
}
|
||||
long now = System.currentTimeMillis();
|
||||
Bounty bounty = new Bounty(UUID.randomUUID(), target.getUniqueId(), safeName(target), amount,
|
||||
now, now, defaultExpiresAt(now), List.of());
|
||||
bounties.put(target.getUniqueId(), bounty);
|
||||
save();
|
||||
history.record("ADMIN_SET", target.getUniqueId(), safeName(target), actorUuid(sender), sender.getName(), amount, "Admin set");
|
||||
messages.send(sender, "bounty.set", bountyPlaceholders(bounty));
|
||||
webhooks.admin(sender.getName(), "Set bounty on " + safeName(target) + " to " + economy.format(amount));
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean adminRemove(CommandSender sender, OfflinePlayer target, String type, double refundPercent) {
|
||||
if (target == null) {
|
||||
messages.send(sender, "invalid-player");
|
||||
return false;
|
||||
}
|
||||
Bounty bounty = bounties.remove(target.getUniqueId());
|
||||
if (bounty == null) {
|
||||
messages.send(sender, "bounty-not-found", Map.of("target", safeName(target)));
|
||||
return false;
|
||||
}
|
||||
refundContributions(bounty, refundPercent, configService.main().getBoolean("refunds.refund-fees", false), sender);
|
||||
save();
|
||||
history.record(type, bounty.targetUuid(), bounty.targetName(), actorUuid(sender), sender.getName(), bounty.amount(), "Admin action");
|
||||
messages.send(sender, "bounty.removed", bountyPlaceholders(bounty));
|
||||
webhooks.admin(sender.getName(), type + " bounty on " + bounty.targetName() + " (" + economy.format(bounty.amount()) + ")");
|
||||
return true;
|
||||
}
|
||||
|
||||
public int clearAll(CommandSender sender) {
|
||||
int count = bounties.size();
|
||||
double refund = configService.main().getDouble("refunds.on-admin-remove-percent", 100.0);
|
||||
for (Bounty bounty : new ArrayList<>(bounties.values())) {
|
||||
refundContributions(bounty, refund, configService.main().getBoolean("refunds.refund-fees", false), sender);
|
||||
history.record("ADMIN_REMOVE", bounty.targetUuid(), bounty.targetName(), actorUuid(sender), sender.getName(), bounty.amount(), "Clear all");
|
||||
}
|
||||
bounties.clear();
|
||||
save();
|
||||
webhooks.admin(sender.getName(), "Cleared all active bounties: " + count);
|
||||
return count;
|
||||
}
|
||||
|
||||
public int cleanupExpired() {
|
||||
if (!configService.main().getBoolean("expiration.enabled", true)) {
|
||||
return 0;
|
||||
}
|
||||
long now = System.currentTimeMillis();
|
||||
int removed = 0;
|
||||
Iterator<Map.Entry<UUID, Bounty>> iterator = bounties.entrySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Bounty bounty = iterator.next().getValue();
|
||||
if (bounty.isExpired(now) || shouldExpireForTargetState(bounty)) {
|
||||
iterator.remove();
|
||||
double refund = configService.main().getDouble("refunds.on-expire-percent", 50.0);
|
||||
refundContributions(bounty, refund, configService.main().getBoolean("refunds.refund-fees", false), Bukkit.getConsoleSender());
|
||||
history.record("EXPIRED", bounty.targetUuid(), bounty.targetName(), null, "Console", bounty.amount(), "Expired");
|
||||
if (configService.main().getBoolean("logging.console.admin-actions", true)) {
|
||||
plugin.getLogger().info("Expired bounty on " + bounty.targetName() + " worth " + bounty.amount());
|
||||
}
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
if (removed > 0) {
|
||||
save();
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
public void handleDeath(Player victim, Player killer) {
|
||||
Bounty bounty = bounties.get(victim.getUniqueId());
|
||||
if (bounty == null) {
|
||||
return;
|
||||
}
|
||||
if (killer == null) {
|
||||
if (configService.main().getBoolean("claim-rules.block-environmental-deaths", true)
|
||||
&& configService.main().getBoolean("anti-abuse.log-failed-claims", true)) {
|
||||
history.suspicious("ENVIRONMENTAL_DEATH", null, victim, "Bounty target died without a player killer.", 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
ClaimValidation validation = validateClaim(killer, victim, bounty);
|
||||
if (!validation.allowed()) {
|
||||
Map<String, String> placeholders = new HashMap<>();
|
||||
placeholders.put("reason", messages.raw(validation.messageKey(), validation.reason()));
|
||||
messages.send(killer, "claim-denied-format", placeholders);
|
||||
if (configService.main().getBoolean("anti-abuse.log-failed-claims", true)) {
|
||||
history.suspicious("CLAIM_DENIED", killer, victim, validation.reason(), severity(validation.reason()));
|
||||
runSuspiciousCommands(killer, victim, validation.reason());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!economy.isReady()) {
|
||||
messages.send(killer, "economy-missing");
|
||||
plugin.getLogger().warning("Could not pay bounty claim because economy is missing.");
|
||||
return;
|
||||
}
|
||||
|
||||
double payout = claimPayout(bounty.amount());
|
||||
EconomyService.EconomyResult deposit = economy.deposit(killer, payout);
|
||||
if (!deposit.success()) {
|
||||
plugin.getLogger().warning("Could not pay bounty claim to " + killer.getName() + ": " + deposit.message());
|
||||
return;
|
||||
}
|
||||
|
||||
bounties.remove(victim.getUniqueId());
|
||||
save();
|
||||
antiAbuse.recordClaim(killer, victim);
|
||||
history.record("CLAIMED", victim.getUniqueId(), victim.getName(), killer.getUniqueId(), killer.getName(),
|
||||
payout, "Killed bounty target");
|
||||
|
||||
Map<String, String> placeholders = bountyPlaceholders(bounty);
|
||||
placeholders.put("killer", killer.getName());
|
||||
placeholders.put("payout", economy.format(payout));
|
||||
messages.send(killer, "bounty.claimed", placeholders);
|
||||
if (configService.main().getBoolean("economy.broadcasts.enabled", true)
|
||||
&& payout >= configService.main().getDouble("economy.broadcasts.claimed-threshold", 5000.0)) {
|
||||
messages.broadcast("bounty.claim-broadcast", placeholders);
|
||||
}
|
||||
if (configService.main().getBoolean("logging.console.claims", true)) {
|
||||
plugin.getLogger().info(killer.getName() + " claimed bounty on " + victim.getName() + " for " + payout);
|
||||
}
|
||||
webhooks.claim(killer.getName(), victim.getName(), payout);
|
||||
}
|
||||
|
||||
public ClaimValidation validateClaim(Player killer, Player target, Bounty bounty) {
|
||||
if (configService.main().getBoolean("claim-rules.require-permission", true)
|
||||
&& !killer.hasPermission("dirtbounties.claim")) {
|
||||
return ClaimValidation.denied("claim-denied.no-permission", "No claim permission");
|
||||
}
|
||||
if (killer.getUniqueId().equals(target.getUniqueId())) {
|
||||
return ClaimValidation.denied("claim-denied.self", "Self claim");
|
||||
}
|
||||
|
||||
boolean bypass = configService.main().getBoolean("claim-rules.allow-bypass-permission", true)
|
||||
&& killer.hasPermission("dirtbounties.bypass.claimrules");
|
||||
if (bypass) {
|
||||
return ClaimValidation.allow();
|
||||
}
|
||||
|
||||
if (blockedWorld(target.getWorld())) {
|
||||
return ClaimValidation.denied("claim-denied.world", "World blocked");
|
||||
}
|
||||
if (blockedGameMode(killer.getGameMode())) {
|
||||
return ClaimValidation.denied("claim-denied.gamemode", "Blocked game mode");
|
||||
}
|
||||
if (configService.main().getBoolean("claim-rules.prevent-same-ip-claims", true)
|
||||
&& playerCache.sameLastIp(killer.getUniqueId(), target.getUniqueId())) {
|
||||
return ClaimValidation.denied("claim-denied.same-ip", "Same last IP");
|
||||
}
|
||||
if (configService.main().getBoolean("claim-rules.prevent-shared-known-ip-claims", true)
|
||||
&& playerCache.sharedKnownIp(killer.getUniqueId(), target.getUniqueId())) {
|
||||
return ClaimValidation.denied("claim-denied.shared-known-ip", "Shared known IP");
|
||||
}
|
||||
if (!combatTracker.meetsRequirement(killer, target)) {
|
||||
return ClaimValidation.denied("claim-denied.combat", "Combat requirement failed");
|
||||
}
|
||||
|
||||
ClaimValidation abuse = antiAbuse.validate(killer, target);
|
||||
if (!abuse.allowed()) {
|
||||
return abuse;
|
||||
}
|
||||
|
||||
return bounty == null
|
||||
? ClaimValidation.denied("claim-denied.no-bounty", "No bounty")
|
||||
: ClaimValidation.allow();
|
||||
}
|
||||
|
||||
public Map<String, String> bountyPlaceholders(Bounty bounty) {
|
||||
Map<String, String> placeholders = new HashMap<>();
|
||||
placeholders.put("target", bounty.targetName());
|
||||
placeholders.put("amount", economy.format(bounty.amount()));
|
||||
placeholders.put("total", economy.format(bounty.amount()));
|
||||
placeholders.put("contributors", String.valueOf(bounty.contributions().size()));
|
||||
placeholders.put("reason", bounty.topReason(configService.main().getString("bounties.default-reason", "No reason given.")));
|
||||
placeholders.put("expires", expiresText(bounty));
|
||||
return placeholders;
|
||||
}
|
||||
|
||||
public String expiresText(Bounty bounty) {
|
||||
if (bounty.expiresAt() <= 0L) {
|
||||
return "never";
|
||||
}
|
||||
long remaining = bounty.expiresAt() - System.currentTimeMillis();
|
||||
return remaining <= 0L ? "expired" : TimeUtil.formatDuration(remaining);
|
||||
}
|
||||
|
||||
public String formatAmount(double amount) {
|
||||
return economy.format(amount);
|
||||
}
|
||||
|
||||
public Fee previewFee(double amount, boolean increase) {
|
||||
return calculateFee(amount, increase);
|
||||
}
|
||||
|
||||
private boolean validateTargetForPlacement(Player placer, OfflinePlayer target) {
|
||||
if (target == null) {
|
||||
messages.send(placer, "invalid-player");
|
||||
return false;
|
||||
}
|
||||
if (!configService.main().getBoolean("bounties.allow-offline-targets", true) && !target.isOnline()) {
|
||||
messages.send(placer, "invalid-player");
|
||||
return false;
|
||||
}
|
||||
if (!configService.main().getBoolean("bounties.allow-never-joined-targets", false)
|
||||
&& !target.hasPlayedBefore()
|
||||
&& !target.isOnline()) {
|
||||
messages.send(placer, "invalid-player");
|
||||
return false;
|
||||
}
|
||||
if (!configService.main().getBoolean("bounties.allow-self-target", false)
|
||||
&& placer.getUniqueId().equals(target.getUniqueId())) {
|
||||
messages.send(placer, "target-self");
|
||||
return false;
|
||||
}
|
||||
if (!configService.main().getBoolean("bounties.allow-banned-targets", false) && target.isBanned()) {
|
||||
messages.send(placer, "target-banned");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private String cleanReason(Player player, String reason) {
|
||||
if (!configService.main().getBoolean("bounties.allow-reasons", true)) {
|
||||
return configService.main().getString("bounties.default-reason", "No reason given.");
|
||||
}
|
||||
String cleaned = reason == null || reason.isBlank()
|
||||
? configService.main().getString("bounties.default-reason", "No reason given.")
|
||||
: reason.trim();
|
||||
int max = configService.main().getInt("bounties.max-reason-length", 80);
|
||||
if (cleaned.length() > max) {
|
||||
messages.send(player, "reason-too-long", Map.of("max", String.valueOf(max)));
|
||||
return null;
|
||||
}
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
private boolean sanitizeAnonymous(Player player, boolean requested) {
|
||||
if (!requested || !configService.main().getBoolean("bounties.allow-anonymous", true)) {
|
||||
return false;
|
||||
}
|
||||
if (configService.main().getBoolean("bounties.anonymous-requires-permission", true)
|
||||
&& !player.hasPermission("dirtbounties.anonymous")) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private Fee calculateFee(double amount, boolean increase) {
|
||||
double percent = configService.main().getDouble(increase ? "economy.fees.add-percent" : "economy.fees.placement-percent", 5.0);
|
||||
String mode = configService.main().getString(increase ? "economy.fees.add-mode" : "economy.fees.placement-mode", "extra");
|
||||
double fee = NumberUtil.clampMoney(amount * Math.max(0.0, percent) / 100.0);
|
||||
double bountyAmount = "deduct".equalsIgnoreCase(mode) ? NumberUtil.clampMoney(amount - fee) : amount;
|
||||
bountyAmount = Math.max(0.0, bountyAmount);
|
||||
double totalCost = "deduct".equalsIgnoreCase(mode) ? amount : NumberUtil.clampMoney(amount + fee);
|
||||
return new Fee(NumberUtil.clampMoney(bountyAmount), fee, NumberUtil.clampMoney(totalCost));
|
||||
}
|
||||
|
||||
private void maybeBroadcastPlacement(boolean newBounty, double previousTotal, Bounty bounty, Map<String, String> placeholders) {
|
||||
if (!configService.main().getBoolean("economy.broadcasts.enabled", true)) {
|
||||
return;
|
||||
}
|
||||
double threshold = configService.main().getDouble("economy.broadcasts.placed-threshold", 5000.0);
|
||||
if (bounty.amount() >= threshold) {
|
||||
messages.broadcast(newBounty ? "bounty.broadcast-placed" : "bounty.broadcast-added", placeholders);
|
||||
}
|
||||
for (Object object : configService.main().getList("economy.broadcasts.milestone-thresholds", List.of())) {
|
||||
double milestone = parseDouble(object);
|
||||
if (milestone > 0.0 && previousTotal < milestone && bounty.amount() >= milestone) {
|
||||
messages.broadcast("bounty.milestone", placeholders);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void refundContributions(Bounty bounty, double percent, boolean includeFees, CommandSender actor) {
|
||||
if (!economy.isReady() || percent <= 0.0) {
|
||||
return;
|
||||
}
|
||||
boolean refundOffline = configService.main().getBoolean("refunds.refund-offline-players", true);
|
||||
double minimumRefund = configService.main().getDouble("refunds.minimum-refund", 0.01);
|
||||
for (BountyContribution contribution : bounty.contributions()) {
|
||||
if (contribution.placerUuid() == null) {
|
||||
continue;
|
||||
}
|
||||
OfflinePlayer player = Bukkit.getOfflinePlayer(contribution.placerUuid());
|
||||
if (!refundOffline && !player.isOnline()) {
|
||||
continue;
|
||||
}
|
||||
double refund = NumberUtil.clampMoney((contribution.amount() + (includeFees ? contribution.fee() : 0.0)) * percent / 100.0);
|
||||
if (refund < minimumRefund) {
|
||||
continue;
|
||||
}
|
||||
EconomyService.EconomyResult result = economy.deposit(player, refund);
|
||||
if (result.success()) {
|
||||
history.record("REFUND", bounty.targetUuid(), bounty.targetName(), contribution.placerUuid(),
|
||||
player.getName() == null ? contribution.placerName() : player.getName(), refund, "Refunded by " + actor.getName());
|
||||
} else {
|
||||
plugin.getLogger().warning("Failed bounty refund to " + contribution.placerName() + ": " + result.message());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private long defaultExpiresAt(long now) {
|
||||
if (!configService.main().getBoolean("expiration.enabled", true)) {
|
||||
return 0L;
|
||||
}
|
||||
long duration = TimeUtil.parseMillis(configService.main().getString("expiration.default-duration"), 1_209_600_000L);
|
||||
long max = TimeUtil.parseMillis(configService.main().getString("expiration.max-duration"), 2_592_000_000L);
|
||||
if (max > 0L) {
|
||||
duration = Math.min(duration, max);
|
||||
}
|
||||
return duration <= 0L ? 0L : now + duration;
|
||||
}
|
||||
|
||||
private boolean shouldExpireForTargetState(Bounty bounty) {
|
||||
OfflinePlayer target = Bukkit.getOfflinePlayer(bounty.targetUuid());
|
||||
if ("expire".equalsIgnoreCase(configService.main().getString("expiration.banned-player-action", "keep"))
|
||||
&& target.isBanned()) {
|
||||
return true;
|
||||
}
|
||||
return "expire".equalsIgnoreCase(configService.main().getString("expiration.deleted-player-action", "keep"))
|
||||
&& !target.hasPlayedBefore()
|
||||
&& !target.isOnline();
|
||||
}
|
||||
|
||||
private boolean blockedWorld(World world) {
|
||||
String mode = configService.main().getString("claim-rules.worlds.mode", "blacklist");
|
||||
if (mode == null || mode.equalsIgnoreCase("disabled")) {
|
||||
return false;
|
||||
}
|
||||
boolean listed = configService.main().getStringList("claim-rules.worlds.list").stream()
|
||||
.anyMatch(name -> name.equalsIgnoreCase(world.getName()));
|
||||
if (mode.equalsIgnoreCase("whitelist")) {
|
||||
return !listed;
|
||||
}
|
||||
if (mode.equalsIgnoreCase("blacklist")) {
|
||||
return listed;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean blockedGameMode(GameMode mode) {
|
||||
for (String configured : configService.main().getStringList("claim-rules.blocked-killer-game-modes")) {
|
||||
if (configured.equalsIgnoreCase(mode.name())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private double claimPayout(double amount) {
|
||||
double tax = Math.max(0.0, configService.main().getDouble("economy.fees.claim-tax-percent", 0.0));
|
||||
double sink = Math.max(0.0, configService.main().getDouble("economy.fees.claim-sink-percent", 0.0));
|
||||
double removed = Math.min(100.0, tax + sink);
|
||||
return NumberUtil.clampMoney(amount * (100.0 - removed) / 100.0);
|
||||
}
|
||||
|
||||
private int severity(String reason) {
|
||||
String lower = reason == null ? "" : reason.toLowerCase(Locale.ROOT);
|
||||
if (lower.contains("ip")) {
|
||||
return 5;
|
||||
}
|
||||
if (lower.contains("cooldown") || lower.contains("limit")) {
|
||||
return 3;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
private void runSuspiciousCommands(Player killer, Player target, String reason) {
|
||||
if (!configService.main().getBoolean("anti-abuse.run-console-commands-on-suspicious", false)) {
|
||||
return;
|
||||
}
|
||||
for (String command : configService.main().getStringList("anti-abuse.suspicious-commands")) {
|
||||
String parsed = command
|
||||
.replace("{killer}", killer.getName())
|
||||
.replace("{target}", target.getName())
|
||||
.replace("{reason}", reason == null ? "" : reason);
|
||||
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), parsed);
|
||||
}
|
||||
}
|
||||
|
||||
private UUID actorUuid(CommandSender sender) {
|
||||
return sender instanceof Player player ? player.getUniqueId() : null;
|
||||
}
|
||||
|
||||
private String safeName(OfflinePlayer player) {
|
||||
return player.getName() == null ? player.getUniqueId().toString() : player.getName();
|
||||
}
|
||||
|
||||
private double parseDouble(Object object) {
|
||||
if (object instanceof Number number) {
|
||||
return number.doubleValue();
|
||||
}
|
||||
if (object instanceof String string) {
|
||||
try {
|
||||
return Double.parseDouble(string);
|
||||
} catch (NumberFormatException ignored) {
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
public record Fee(double bountyAmount, double feeAmount, double totalCost) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package com.dirtbagmc.dirtbounties.service;
|
||||
|
||||
import com.dirtbagmc.dirtbounties.config.ConfigService;
|
||||
import com.dirtbagmc.dirtbounties.util.TimeUtil;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class CombatTracker {
|
||||
private final ConfigService configService;
|
||||
private final Map<String, CombatRecord> records = new HashMap<>();
|
||||
|
||||
public CombatTracker(ConfigService configService) {
|
||||
this.configService = configService;
|
||||
}
|
||||
|
||||
public void record(Player attacker, Player victim, double damage) {
|
||||
if (attacker.getUniqueId().equals(victim.getUniqueId())) {
|
||||
return;
|
||||
}
|
||||
long now = System.currentTimeMillis();
|
||||
String key = key(attacker.getUniqueId(), victim.getUniqueId());
|
||||
CombatRecord record = records.computeIfAbsent(key, ignored -> new CombatRecord(now));
|
||||
record.lastAt(now);
|
||||
record.damage(record.damage() + Math.max(0.0, damage));
|
||||
record.hits(record.hits() + 1);
|
||||
cleanup(now);
|
||||
}
|
||||
|
||||
public boolean meetsRequirement(Player attacker, Player victim) {
|
||||
if (!configService.main().getBoolean("claim-rules.combat.enabled", true)) {
|
||||
return true;
|
||||
}
|
||||
long now = System.currentTimeMillis();
|
||||
cleanup(now);
|
||||
CombatRecord record = records.get(key(attacker.getUniqueId(), victim.getUniqueId()));
|
||||
if (record == null) {
|
||||
return false;
|
||||
}
|
||||
long window = TimeUtil.parseMillis(configService.main().getString("claim-rules.combat.window"), 30_000L);
|
||||
if (now - record.lastAt() > window) {
|
||||
return false;
|
||||
}
|
||||
double minDamage = configService.main().getDouble("claim-rules.combat.min-damage", 4.0);
|
||||
int minHits = configService.main().getInt("claim-rules.combat.min-hits", 1);
|
||||
long minDuration = TimeUtil.parseMillis(configService.main().getString("claim-rules.combat.min-combat-duration"), 0L);
|
||||
return record.damage() >= minDamage
|
||||
&& record.hits() >= minHits
|
||||
&& record.lastAt() - record.firstAt() >= minDuration;
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
records.clear();
|
||||
}
|
||||
|
||||
private void cleanup(long now) {
|
||||
long window = TimeUtil.parseMillis(configService.main().getString("claim-rules.combat.window"), 30_000L);
|
||||
Iterator<Map.Entry<String, CombatRecord>> iterator = records.entrySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
if (now - iterator.next().getValue().lastAt() > window) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String key(UUID attacker, UUID victim) {
|
||||
return attacker + ":" + victim;
|
||||
}
|
||||
|
||||
private static final class CombatRecord {
|
||||
private final long firstAt;
|
||||
private long lastAt;
|
||||
private double damage;
|
||||
private int hits;
|
||||
|
||||
private CombatRecord(long firstAt) {
|
||||
this.firstAt = firstAt;
|
||||
this.lastAt = firstAt;
|
||||
}
|
||||
|
||||
public long firstAt() {
|
||||
return firstAt;
|
||||
}
|
||||
|
||||
public long lastAt() {
|
||||
return lastAt;
|
||||
}
|
||||
|
||||
public void lastAt(long lastAt) {
|
||||
this.lastAt = lastAt;
|
||||
}
|
||||
|
||||
public double damage() {
|
||||
return damage;
|
||||
}
|
||||
|
||||
public void damage(double damage) {
|
||||
this.damage = damage;
|
||||
}
|
||||
|
||||
public int hits() {
|
||||
return hits;
|
||||
}
|
||||
|
||||
public void hits(int hits) {
|
||||
this.hits = hits;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package com.dirtbagmc.dirtbounties.service;
|
||||
|
||||
import com.dirtbagmc.dirtbounties.config.ConfigService;
|
||||
import com.dirtbagmc.dirtbounties.model.BountyHistoryRecord;
|
||||
import com.dirtbagmc.dirtbounties.model.SuspiciousActivity;
|
||||
import com.dirtbagmc.dirtbounties.storage.StorageManager;
|
||||
import com.dirtbagmc.dirtbounties.util.TimeUtil;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class HistoryService {
|
||||
private final ConfigService configService;
|
||||
private final StorageManager storageManager;
|
||||
private final List<BountyHistoryRecord> records = new ArrayList<>();
|
||||
private final List<SuspiciousActivity> suspicious = new ArrayList<>();
|
||||
|
||||
public HistoryService(ConfigService configService, StorageManager storageManager) {
|
||||
this.configService = configService;
|
||||
this.storageManager = storageManager;
|
||||
}
|
||||
|
||||
public void load() {
|
||||
records.clear();
|
||||
suspicious.clear();
|
||||
records.addAll(storageManager.loadHistory());
|
||||
suspicious.addAll(storageManager.loadSuspicious());
|
||||
prune();
|
||||
}
|
||||
|
||||
public void save() {
|
||||
storageManager.saveHistory(records);
|
||||
storageManager.saveSuspicious(suspicious);
|
||||
}
|
||||
|
||||
public void record(String type, UUID targetUuid, String targetName, UUID actorUuid, String actorName,
|
||||
double amount, String note) {
|
||||
if (!configService.main().getBoolean("history.enabled", true)) {
|
||||
return;
|
||||
}
|
||||
records.add(0, new BountyHistoryRecord(UUID.randomUUID(), type, targetUuid, targetName,
|
||||
actorUuid, actorName, amount, note, System.currentTimeMillis()));
|
||||
prune();
|
||||
if (configService.main().getBoolean("storage.write-history-to-disk-immediately", true)) {
|
||||
storageManager.saveHistory(records);
|
||||
}
|
||||
}
|
||||
|
||||
public void suspicious(String type, OfflinePlayer killer, OfflinePlayer target, String details, int severity) {
|
||||
suspicious.add(0, new SuspiciousActivity(UUID.randomUUID(), type,
|
||||
killer == null ? null : killer.getUniqueId(),
|
||||
killer == null ? "Unknown" : safeName(killer),
|
||||
target == null ? null : target.getUniqueId(),
|
||||
target == null ? "Unknown" : safeName(target),
|
||||
details,
|
||||
severity,
|
||||
System.currentTimeMillis()));
|
||||
prune();
|
||||
if (configService.main().getBoolean("storage.write-history-to-disk-immediately", true)) {
|
||||
storageManager.saveSuspicious(suspicious);
|
||||
}
|
||||
}
|
||||
|
||||
public List<BountyHistoryRecord> forPlayer(UUID uuid, int limit) {
|
||||
return records.stream()
|
||||
.filter(record -> uuid.equals(record.targetUuid()) || uuid.equals(record.actorUuid()))
|
||||
.sorted(Comparator.comparingLong(BountyHistoryRecord::createdAt).reversed())
|
||||
.limit(limit)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public List<BountyHistoryRecord> recent(int limit) {
|
||||
return records.stream()
|
||||
.sorted(Comparator.comparingLong(BountyHistoryRecord::createdAt).reversed())
|
||||
.limit(limit)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public List<SuspiciousActivity> suspiciousRecent(int limit) {
|
||||
return suspicious.stream()
|
||||
.sorted(Comparator.comparingLong(SuspiciousActivity::createdAt).reversed())
|
||||
.limit(limit)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public int claimCount(UUID killer, UUID target, long since) {
|
||||
int count = 0;
|
||||
for (BountyHistoryRecord record : records) {
|
||||
if ("CLAIMED".equalsIgnoreCase(record.type())
|
||||
&& killer.equals(record.actorUuid())
|
||||
&& target.equals(record.targetUuid())
|
||||
&& record.createdAt() >= since) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
public long lastClaimByKiller(UUID killer) {
|
||||
return records.stream()
|
||||
.filter(record -> "CLAIMED".equalsIgnoreCase(record.type()) && killer.equals(record.actorUuid()))
|
||||
.mapToLong(BountyHistoryRecord::createdAt)
|
||||
.max()
|
||||
.orElse(0L);
|
||||
}
|
||||
|
||||
public long lastClaimOnTarget(UUID target) {
|
||||
return records.stream()
|
||||
.filter(record -> "CLAIMED".equalsIgnoreCase(record.type()) && target.equals(record.targetUuid()))
|
||||
.mapToLong(BountyHistoryRecord::createdAt)
|
||||
.max()
|
||||
.orElse(0L);
|
||||
}
|
||||
|
||||
public long lastPairClaim(UUID killer, UUID target) {
|
||||
return records.stream()
|
||||
.filter(record -> "CLAIMED".equalsIgnoreCase(record.type())
|
||||
&& killer.equals(record.actorUuid())
|
||||
&& target.equals(record.targetUuid()))
|
||||
.mapToLong(BountyHistoryRecord::createdAt)
|
||||
.max()
|
||||
.orElse(0L);
|
||||
}
|
||||
|
||||
public long claimedCountBy(UUID killer) {
|
||||
return records.stream()
|
||||
.filter(record -> "CLAIMED".equalsIgnoreCase(record.type()) && killer.equals(record.actorUuid()))
|
||||
.count();
|
||||
}
|
||||
|
||||
private void prune() {
|
||||
long olderThan = TimeUtil.parseMillis(configService.main().getString("history.prune-older-than"), 7_776_000_000L);
|
||||
if (olderThan > 0L) {
|
||||
long cutoff = System.currentTimeMillis() - olderThan;
|
||||
records.removeIf(record -> record.createdAt() < cutoff);
|
||||
suspicious.removeIf(record -> record.createdAt() < cutoff);
|
||||
}
|
||||
|
||||
int maxRecords = configService.main().getInt("history.max-records", 2000);
|
||||
trim(records, maxRecords);
|
||||
|
||||
int maxSuspicious = configService.main().getInt("anti-abuse.suspicious-history-limit", 500);
|
||||
trim(suspicious, maxSuspicious);
|
||||
}
|
||||
|
||||
private <T> void trim(List<T> list, int max) {
|
||||
if (max <= 0) {
|
||||
list.clear();
|
||||
return;
|
||||
}
|
||||
while (list.size() > max) {
|
||||
list.remove(list.size() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
private String safeName(OfflinePlayer player) {
|
||||
return player.getName() == null ? player.getUniqueId().toString() : player.getName();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package com.dirtbagmc.dirtbounties.service;
|
||||
|
||||
import com.dirtbagmc.dirtbounties.config.ConfigService;
|
||||
import com.dirtbagmc.dirtbounties.util.ColorUtil;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.Registry;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
public final class MessageService {
|
||||
private final ConfigService configService;
|
||||
|
||||
public MessageService(ConfigService configService) {
|
||||
this.configService = configService;
|
||||
}
|
||||
|
||||
public void send(CommandSender sender, String path) {
|
||||
send(sender, path, Map.of());
|
||||
}
|
||||
|
||||
public void send(CommandSender sender, String path, Map<String, String> placeholders) {
|
||||
String raw = configService.messages().getString(path);
|
||||
if (raw == null || raw.isBlank()) {
|
||||
return;
|
||||
}
|
||||
sender.sendMessage(component(raw, placeholders));
|
||||
}
|
||||
|
||||
public void sendLines(CommandSender sender, String path, Map<String, String> placeholders) {
|
||||
List<String> lines = configService.messages().getStringList(path);
|
||||
if (lines.isEmpty()) {
|
||||
send(sender, path, placeholders);
|
||||
return;
|
||||
}
|
||||
for (String line : lines) {
|
||||
sender.sendMessage(component(line, placeholders));
|
||||
}
|
||||
}
|
||||
|
||||
public void broadcast(String path, Map<String, String> placeholders) {
|
||||
String raw = configService.messages().getString(path);
|
||||
if (raw == null || raw.isBlank()) {
|
||||
return;
|
||||
}
|
||||
Component component = component(raw, placeholders);
|
||||
Bukkit.getOnlinePlayers().forEach(player -> player.sendMessage(component));
|
||||
Bukkit.getConsoleSender().sendMessage(component);
|
||||
}
|
||||
|
||||
public Component component(String raw, Map<String, String> placeholders) {
|
||||
return ColorUtil.component(apply(raw, placeholders));
|
||||
}
|
||||
|
||||
public String legacy(String raw, Map<String, String> placeholders) {
|
||||
return ColorUtil.legacySection(apply(raw, placeholders));
|
||||
}
|
||||
|
||||
public List<Component> components(List<String> lines, Map<String, String> placeholders) {
|
||||
return lines.stream().map(line -> component(line, placeholders)).toList();
|
||||
}
|
||||
|
||||
public String raw(String path, String fallback) {
|
||||
return configService.messages().getString(path, fallback);
|
||||
}
|
||||
|
||||
public void play(Player player, String configPath) {
|
||||
String soundName = configService.main().getString(configPath, "");
|
||||
if (soundName == null || soundName.isBlank() || soundName.equalsIgnoreCase("none")) {
|
||||
return;
|
||||
}
|
||||
Sound sound = resolveSound(soundName);
|
||||
if (sound != null) {
|
||||
player.playSound(player.getLocation(), sound, 1.0f, 1.0f);
|
||||
} else if (configService.debug()) {
|
||||
Bukkit.getLogger().warning("[DirtBounties] Unknown sound in config: " + soundName);
|
||||
}
|
||||
}
|
||||
|
||||
private Sound resolveSound(String soundName) {
|
||||
String normalized = soundName.toLowerCase(Locale.ROOT);
|
||||
NamespacedKey key = normalized.contains(":")
|
||||
? NamespacedKey.fromString(normalized)
|
||||
: NamespacedKey.minecraft(normalized.replace('_', '.'));
|
||||
return key == null ? null : Registry.SOUNDS.get(key);
|
||||
}
|
||||
|
||||
private String apply(String raw, Map<String, String> placeholders) {
|
||||
Map<String, String> merged = new HashMap<>();
|
||||
merged.put("prefix", configService.messages().getString("prefix", ""));
|
||||
if (placeholders != null) {
|
||||
merged.putAll(placeholders);
|
||||
}
|
||||
return ColorUtil.replace(raw, merged);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package com.dirtbagmc.dirtbounties.service;
|
||||
|
||||
import com.dirtbagmc.dirtbounties.config.ConfigService;
|
||||
import com.dirtbagmc.dirtbounties.storage.StorageManager;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class PlayerCacheService {
|
||||
private final ConfigService configService;
|
||||
private final StorageManager storageManager;
|
||||
private final Map<UUID, CacheEntry> cache = new HashMap<>();
|
||||
|
||||
public PlayerCacheService(ConfigService configService, StorageManager storageManager) {
|
||||
this.configService = configService;
|
||||
this.storageManager = storageManager;
|
||||
}
|
||||
|
||||
public void load() {
|
||||
cache.clear();
|
||||
storageManager.loadPlayerCache().forEach((uuid, data) ->
|
||||
cache.put(uuid, new CacheEntry(data.name(), data.lastIp(), new HashSet<>(data.knownIps()), data.lastSeen())));
|
||||
}
|
||||
|
||||
public void save() {
|
||||
if (!configService.main().getBoolean("storage.save-player-ip-cache", true)) {
|
||||
return;
|
||||
}
|
||||
Map<UUID, StorageManager.PlayerCacheData> data = new HashMap<>();
|
||||
cache.forEach((uuid, entry) ->
|
||||
data.put(uuid, new StorageManager.PlayerCacheData(entry.name(), entry.lastIp(), entry.knownIps(), entry.lastSeen())));
|
||||
storageManager.savePlayerCache(data);
|
||||
}
|
||||
|
||||
public void update(Player player) {
|
||||
String ip = ip(player);
|
||||
CacheEntry entry = cache.computeIfAbsent(player.getUniqueId(),
|
||||
ignored -> new CacheEntry(player.getName(), "", new HashSet<>(), 0L));
|
||||
entry.name(player.getName());
|
||||
entry.lastSeen(System.currentTimeMillis());
|
||||
if (!ip.isBlank()) {
|
||||
entry.lastIp(ip);
|
||||
entry.knownIps().add(ip);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean sameLastIp(UUID first, UUID second) {
|
||||
CacheEntry left = cache.get(first);
|
||||
CacheEntry right = cache.get(second);
|
||||
return left != null && right != null && !left.lastIp().isBlank() && left.lastIp().equals(right.lastIp());
|
||||
}
|
||||
|
||||
public boolean sharedKnownIp(UUID first, UUID second) {
|
||||
CacheEntry left = cache.get(first);
|
||||
CacheEntry right = cache.get(second);
|
||||
if (left == null || right == null) {
|
||||
return false;
|
||||
}
|
||||
for (String ip : left.knownIps()) {
|
||||
if (!ip.isBlank() && right.knownIps().contains(ip)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public String cachedName(UUID uuid, String fallback) {
|
||||
CacheEntry entry = cache.get(uuid);
|
||||
return entry == null || entry.name().isBlank() ? fallback : entry.name();
|
||||
}
|
||||
|
||||
private String ip(Player player) {
|
||||
InetSocketAddress address = player.getAddress();
|
||||
if (address == null || address.getAddress() == null) {
|
||||
return "";
|
||||
}
|
||||
return address.getAddress().getHostAddress();
|
||||
}
|
||||
|
||||
private static final class CacheEntry {
|
||||
private String name;
|
||||
private String lastIp;
|
||||
private final Set<String> knownIps;
|
||||
private long lastSeen;
|
||||
|
||||
private CacheEntry(String name, String lastIp, Set<String> knownIps, long lastSeen) {
|
||||
this.name = name;
|
||||
this.lastIp = lastIp == null ? "" : lastIp;
|
||||
this.knownIps = knownIps;
|
||||
this.lastSeen = lastSeen;
|
||||
}
|
||||
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void name(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String lastIp() {
|
||||
return lastIp;
|
||||
}
|
||||
|
||||
public void lastIp(String lastIp) {
|
||||
this.lastIp = lastIp;
|
||||
}
|
||||
|
||||
public Set<String> knownIps() {
|
||||
return knownIps;
|
||||
}
|
||||
|
||||
public long lastSeen() {
|
||||
return lastSeen;
|
||||
}
|
||||
|
||||
public void lastSeen(long lastSeen) {
|
||||
this.lastSeen = lastSeen;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
package com.dirtbagmc.dirtbounties.storage;
|
||||
|
||||
import com.dirtbagmc.dirtbounties.model.Bounty;
|
||||
import com.dirtbagmc.dirtbounties.model.BountyContribution;
|
||||
import com.dirtbagmc.dirtbounties.model.BountyHistoryRecord;
|
||||
import com.dirtbagmc.dirtbounties.model.SuspiciousActivity;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public final class StorageManager {
|
||||
private final JavaPlugin plugin;
|
||||
private File bountiesFile;
|
||||
private File historyFile;
|
||||
private File suspiciousFile;
|
||||
private File playerCacheFile;
|
||||
|
||||
public StorageManager(JavaPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
plugin.getDataFolder().mkdirs();
|
||||
bountiesFile = ensureFile("bounties.yml");
|
||||
historyFile = ensureFile("history.yml");
|
||||
suspiciousFile = ensureFile("suspicious.yml");
|
||||
playerCacheFile = ensureFile("player-cache.yml");
|
||||
}
|
||||
|
||||
public Map<UUID, Bounty> loadBounties() {
|
||||
Map<UUID, Bounty> loaded = new HashMap<>();
|
||||
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(bountiesFile);
|
||||
ConfigurationSection root = yaml.getConfigurationSection("bounties");
|
||||
if (root == null) {
|
||||
return loaded;
|
||||
}
|
||||
|
||||
for (String key : root.getKeys(false)) {
|
||||
ConfigurationSection section = root.getConfigurationSection(key);
|
||||
if (section == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
UUID targetUuid = UUID.fromString(section.getString("target-uuid", key));
|
||||
UUID bountyId = UUID.fromString(section.getString("id", UUID.randomUUID().toString()));
|
||||
List<BountyContribution> contributions = new ArrayList<>();
|
||||
ConfigurationSection contributionsSection = section.getConfigurationSection("contributions");
|
||||
if (contributionsSection != null) {
|
||||
for (String contributionKey : contributionsSection.getKeys(false)) {
|
||||
ConfigurationSection contribution = contributionsSection.getConfigurationSection(contributionKey);
|
||||
if (contribution == null) {
|
||||
continue;
|
||||
}
|
||||
UUID placerUuid = uuidOrNull(contribution.getString("placer-uuid"));
|
||||
contributions.add(new BountyContribution(
|
||||
UUID.fromString(contribution.getString("id", contributionKey)),
|
||||
placerUuid,
|
||||
contribution.getString("placer-name", "Console"),
|
||||
contribution.getDouble("amount"),
|
||||
contribution.getDouble("fee"),
|
||||
contribution.getString("reason", ""),
|
||||
contribution.getBoolean("anonymous"),
|
||||
contribution.getLong("created-at")
|
||||
));
|
||||
}
|
||||
}
|
||||
Bounty bounty = new Bounty(
|
||||
bountyId,
|
||||
targetUuid,
|
||||
section.getString("target-name", "Unknown"),
|
||||
section.getDouble("amount"),
|
||||
section.getLong("created-at"),
|
||||
section.getLong("updated-at"),
|
||||
section.getLong("expires-at"),
|
||||
contributions
|
||||
);
|
||||
loaded.put(targetUuid, bounty);
|
||||
} catch (RuntimeException ex) {
|
||||
plugin.getLogger().log(Level.WARNING, "Skipping invalid bounty entry " + key, ex);
|
||||
}
|
||||
}
|
||||
return loaded;
|
||||
}
|
||||
|
||||
public void saveBounties(Collection<Bounty> bounties) {
|
||||
YamlConfiguration yaml = new YamlConfiguration();
|
||||
for (Bounty bounty : bounties) {
|
||||
String path = "bounties." + bounty.targetUuid();
|
||||
yaml.set(path + ".id", bounty.id().toString());
|
||||
yaml.set(path + ".target-uuid", bounty.targetUuid().toString());
|
||||
yaml.set(path + ".target-name", bounty.targetName());
|
||||
yaml.set(path + ".amount", bounty.amount());
|
||||
yaml.set(path + ".created-at", bounty.createdAt());
|
||||
yaml.set(path + ".updated-at", bounty.updatedAt());
|
||||
yaml.set(path + ".expires-at", bounty.expiresAt());
|
||||
for (BountyContribution contribution : bounty.contributions()) {
|
||||
String contributionPath = path + ".contributions." + contribution.id();
|
||||
yaml.set(contributionPath + ".id", contribution.id().toString());
|
||||
yaml.set(contributionPath + ".placer-uuid", contribution.placerUuid() == null ? null : contribution.placerUuid().toString());
|
||||
yaml.set(contributionPath + ".placer-name", contribution.placerName());
|
||||
yaml.set(contributionPath + ".amount", contribution.amount());
|
||||
yaml.set(contributionPath + ".fee", contribution.fee());
|
||||
yaml.set(contributionPath + ".reason", contribution.reason());
|
||||
yaml.set(contributionPath + ".anonymous", contribution.anonymous());
|
||||
yaml.set(contributionPath + ".created-at", contribution.createdAt());
|
||||
}
|
||||
}
|
||||
save(yaml, bountiesFile);
|
||||
}
|
||||
|
||||
public List<BountyHistoryRecord> loadHistory() {
|
||||
List<BountyHistoryRecord> records = new ArrayList<>();
|
||||
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(historyFile);
|
||||
ConfigurationSection root = yaml.getConfigurationSection("records");
|
||||
if (root == null) {
|
||||
return records;
|
||||
}
|
||||
for (String key : root.getKeys(false)) {
|
||||
ConfigurationSection section = root.getConfigurationSection(key);
|
||||
if (section == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
records.add(new BountyHistoryRecord(
|
||||
UUID.fromString(section.getString("id", key)),
|
||||
section.getString("type", "UNKNOWN"),
|
||||
uuidOrNull(section.getString("target-uuid")),
|
||||
section.getString("target-name", "Unknown"),
|
||||
uuidOrNull(section.getString("actor-uuid")),
|
||||
section.getString("actor-name", "Console"),
|
||||
section.getDouble("amount"),
|
||||
section.getString("note", ""),
|
||||
section.getLong("created-at")
|
||||
));
|
||||
} catch (RuntimeException ex) {
|
||||
plugin.getLogger().log(Level.WARNING, "Skipping invalid history entry " + key, ex);
|
||||
}
|
||||
}
|
||||
records.sort((left, right) -> Long.compare(right.createdAt(), left.createdAt()));
|
||||
return records;
|
||||
}
|
||||
|
||||
public void saveHistory(Collection<BountyHistoryRecord> records) {
|
||||
YamlConfiguration yaml = new YamlConfiguration();
|
||||
for (BountyHistoryRecord record : records) {
|
||||
String path = "records." + record.id();
|
||||
yaml.set(path + ".id", record.id().toString());
|
||||
yaml.set(path + ".type", record.type());
|
||||
yaml.set(path + ".target-uuid", record.targetUuid() == null ? null : record.targetUuid().toString());
|
||||
yaml.set(path + ".target-name", record.targetName());
|
||||
yaml.set(path + ".actor-uuid", record.actorUuid() == null ? null : record.actorUuid().toString());
|
||||
yaml.set(path + ".actor-name", record.actorName());
|
||||
yaml.set(path + ".amount", record.amount());
|
||||
yaml.set(path + ".note", record.note());
|
||||
yaml.set(path + ".created-at", record.createdAt());
|
||||
}
|
||||
save(yaml, historyFile);
|
||||
}
|
||||
|
||||
public List<SuspiciousActivity> loadSuspicious() {
|
||||
List<SuspiciousActivity> records = new ArrayList<>();
|
||||
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(suspiciousFile);
|
||||
ConfigurationSection root = yaml.getConfigurationSection("records");
|
||||
if (root == null) {
|
||||
return records;
|
||||
}
|
||||
for (String key : root.getKeys(false)) {
|
||||
ConfigurationSection section = root.getConfigurationSection(key);
|
||||
if (section == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
records.add(new SuspiciousActivity(
|
||||
UUID.fromString(section.getString("id", key)),
|
||||
section.getString("type", "UNKNOWN"),
|
||||
uuidOrNull(section.getString("killer-uuid")),
|
||||
section.getString("killer-name", "Unknown"),
|
||||
uuidOrNull(section.getString("target-uuid")),
|
||||
section.getString("target-name", "Unknown"),
|
||||
section.getString("details", ""),
|
||||
section.getInt("severity", 1),
|
||||
section.getLong("created-at")
|
||||
));
|
||||
} catch (RuntimeException ex) {
|
||||
plugin.getLogger().log(Level.WARNING, "Skipping invalid suspicious entry " + key, ex);
|
||||
}
|
||||
}
|
||||
records.sort((left, right) -> Long.compare(right.createdAt(), left.createdAt()));
|
||||
return records;
|
||||
}
|
||||
|
||||
public void saveSuspicious(Collection<SuspiciousActivity> records) {
|
||||
YamlConfiguration yaml = new YamlConfiguration();
|
||||
for (SuspiciousActivity record : records) {
|
||||
String path = "records." + record.id();
|
||||
yaml.set(path + ".id", record.id().toString());
|
||||
yaml.set(path + ".type", record.type());
|
||||
yaml.set(path + ".killer-uuid", record.killerUuid() == null ? null : record.killerUuid().toString());
|
||||
yaml.set(path + ".killer-name", record.killerName());
|
||||
yaml.set(path + ".target-uuid", record.targetUuid() == null ? null : record.targetUuid().toString());
|
||||
yaml.set(path + ".target-name", record.targetName());
|
||||
yaml.set(path + ".details", record.details());
|
||||
yaml.set(path + ".severity", record.severity());
|
||||
yaml.set(path + ".created-at", record.createdAt());
|
||||
}
|
||||
save(yaml, suspiciousFile);
|
||||
}
|
||||
|
||||
public Map<UUID, PlayerCacheData> loadPlayerCache() {
|
||||
Map<UUID, PlayerCacheData> cache = new HashMap<>();
|
||||
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(playerCacheFile);
|
||||
ConfigurationSection root = yaml.getConfigurationSection("players");
|
||||
if (root == null) {
|
||||
return cache;
|
||||
}
|
||||
for (String key : root.getKeys(false)) {
|
||||
ConfigurationSection section = root.getConfigurationSection(key);
|
||||
if (section == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
UUID uuid = UUID.fromString(key);
|
||||
cache.put(uuid, new PlayerCacheData(
|
||||
section.getString("name", "Unknown"),
|
||||
section.getString("last-ip", ""),
|
||||
new HashSet<>(section.getStringList("known-ips")),
|
||||
section.getLong("last-seen")
|
||||
));
|
||||
} catch (RuntimeException ex) {
|
||||
plugin.getLogger().log(Level.WARNING, "Skipping invalid player cache entry " + key, ex);
|
||||
}
|
||||
}
|
||||
return cache;
|
||||
}
|
||||
|
||||
public void savePlayerCache(Map<UUID, PlayerCacheData> cache) {
|
||||
YamlConfiguration yaml = new YamlConfiguration();
|
||||
for (Map.Entry<UUID, PlayerCacheData> entry : cache.entrySet()) {
|
||||
String path = "players." + entry.getKey();
|
||||
PlayerCacheData data = entry.getValue();
|
||||
yaml.set(path + ".name", data.name());
|
||||
yaml.set(path + ".last-ip", data.lastIp());
|
||||
yaml.set(path + ".known-ips", new ArrayList<>(data.knownIps()));
|
||||
yaml.set(path + ".last-seen", data.lastSeen());
|
||||
}
|
||||
save(yaml, playerCacheFile);
|
||||
}
|
||||
|
||||
private File ensureFile(String name) {
|
||||
File file = new File(plugin.getDataFolder(), name);
|
||||
if (!file.exists()) {
|
||||
try {
|
||||
if (file.createNewFile()) {
|
||||
plugin.getLogger().fine("Created " + name);
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Could not create " + name, ex);
|
||||
}
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
private void save(YamlConfiguration yaml, File file) {
|
||||
try {
|
||||
yaml.save(file);
|
||||
} catch (IOException ex) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Could not save " + file.getName(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private UUID uuidOrNull(String input) {
|
||||
if (input == null || input.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return UUID.fromString(input);
|
||||
}
|
||||
|
||||
public record PlayerCacheData(String name, String lastIp, Set<String> knownIps, long lastSeen) {
|
||||
public PlayerCacheData {
|
||||
knownIps = knownIps == null ? Set.of() : Set.copyOf(knownIps);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.dirtbagmc.dirtbounties.util;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public final class ColorUtil {
|
||||
private static final Pattern HEX_PATTERN = Pattern.compile("&#([A-Fa-f0-9]{6})");
|
||||
private static final LegacyComponentSerializer LEGACY = LegacyComponentSerializer.builder()
|
||||
.character('&')
|
||||
.hexColors()
|
||||
.build();
|
||||
|
||||
private ColorUtil() {
|
||||
}
|
||||
|
||||
public static Component component(String input) {
|
||||
return LEGACY.deserialize(translateHex(input == null ? "" : input));
|
||||
}
|
||||
|
||||
public static String legacySection(String input) {
|
||||
String translated = translateHex(input == null ? "" : input);
|
||||
StringBuilder builder = new StringBuilder(translated.length());
|
||||
for (int i = 0; i < translated.length(); i++) {
|
||||
char current = translated.charAt(i);
|
||||
if (current == '&' && i + 1 < translated.length()) {
|
||||
builder.append('§');
|
||||
} else {
|
||||
builder.append(current);
|
||||
}
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
public static String replace(String input, Map<String, String> placeholders) {
|
||||
String value = input == null ? "" : input;
|
||||
if (placeholders == null || placeholders.isEmpty()) {
|
||||
return value;
|
||||
}
|
||||
for (Map.Entry<String, String> entry : placeholders.entrySet()) {
|
||||
value = value.replace("{" + entry.getKey() + "}", entry.getValue() == null ? "" : entry.getValue());
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static String translateHex(String input) {
|
||||
Matcher matcher = HEX_PATTERN.matcher(input);
|
||||
StringBuilder builder = new StringBuilder();
|
||||
while (matcher.find()) {
|
||||
String hex = matcher.group(1);
|
||||
StringBuilder replacement = new StringBuilder("&x");
|
||||
for (char c : hex.toCharArray()) {
|
||||
replacement.append('&').append(c);
|
||||
}
|
||||
matcher.appendReplacement(builder, Matcher.quoteReplacement(replacement.toString()));
|
||||
}
|
||||
matcher.appendTail(builder);
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.dirtbagmc.dirtbounties.util;
|
||||
|
||||
import com.dirtbagmc.dirtbounties.service.MessageService;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.inventory.ItemFlag;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.inventory.meta.SkullMeta;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
public final class ItemBuilder {
|
||||
private ItemBuilder() {
|
||||
}
|
||||
|
||||
public static ItemStack fromSection(ConfigurationSection section, MessageService messages,
|
||||
Map<String, String> placeholders, OfflinePlayer skullOwner) {
|
||||
if (section == null) {
|
||||
return new ItemStack(Material.STONE);
|
||||
}
|
||||
Material material = material(section.getString("material", "STONE"));
|
||||
ItemStack item = new ItemStack(material);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta == null) {
|
||||
return item;
|
||||
}
|
||||
|
||||
if (meta instanceof SkullMeta skullMeta && skullOwner != null) {
|
||||
skullMeta.setOwningPlayer(skullOwner);
|
||||
}
|
||||
|
||||
String name = section.getString("name", "");
|
||||
if (!name.isBlank()) {
|
||||
meta.displayName(messages.component(name, placeholders));
|
||||
}
|
||||
|
||||
List<String> lore = new ArrayList<>();
|
||||
for (String line : section.getStringList("lore")) {
|
||||
String expanded = ColorUtil.replace(line, placeholders);
|
||||
if (expanded.contains("\n")) {
|
||||
lore.addAll(List.of(expanded.split("\\n", -1)));
|
||||
} else {
|
||||
lore.add(expanded);
|
||||
}
|
||||
}
|
||||
if (!lore.isEmpty()) {
|
||||
meta.lore(lore.stream().map(ColorUtil::component).toList());
|
||||
}
|
||||
|
||||
meta.addItemFlags(ItemFlag.HIDE_ATTRIBUTES, ItemFlag.HIDE_ENCHANTS, ItemFlag.HIDE_ADDITIONAL_TOOLTIP);
|
||||
item.setItemMeta(meta);
|
||||
return item;
|
||||
}
|
||||
|
||||
public static ItemStack simple(Material material, String name, List<String> lore, MessageService messages,
|
||||
Map<String, String> placeholders) {
|
||||
ItemStack item = new ItemStack(material);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta == null) {
|
||||
return item;
|
||||
}
|
||||
meta.displayName(messages.component(name, placeholders));
|
||||
meta.lore(lore.stream().map(line -> messages.component(line, placeholders)).toList());
|
||||
meta.addItemFlags(ItemFlag.HIDE_ATTRIBUTES, ItemFlag.HIDE_ENCHANTS, ItemFlag.HIDE_ADDITIONAL_TOOLTIP);
|
||||
item.setItemMeta(meta);
|
||||
return item;
|
||||
}
|
||||
|
||||
private static Material material(String name) {
|
||||
if (name == null || name.isBlank()) {
|
||||
return Material.STONE;
|
||||
}
|
||||
Material material = Material.matchMaterial(name.toUpperCase(Locale.ROOT));
|
||||
if (material == null) {
|
||||
Bukkit.getLogger().warning("[DirtBounties] Unknown material in GUI config: " + name);
|
||||
return Material.STONE;
|
||||
}
|
||||
return material;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.dirtbagmc.dirtbounties.util;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
|
||||
public final class NumberUtil {
|
||||
private static final DecimalFormat MONEY = new DecimalFormat("#,##0.##");
|
||||
|
||||
private NumberUtil() {
|
||||
}
|
||||
|
||||
public static double clampMoney(double value) {
|
||||
if (Double.isNaN(value) || Double.isInfinite(value)) {
|
||||
return 0.0;
|
||||
}
|
||||
return Math.round(value * 100.0) / 100.0;
|
||||
}
|
||||
|
||||
public static String compact(double value) {
|
||||
return MONEY.format(clampMoney(value));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.dirtbagmc.dirtbounties.util;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public final class TimeUtil {
|
||||
private static final Pattern TOKEN = Pattern.compile("(\\d+(?:\\.\\d+)?)(ms|s|m|h|d|w)", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private TimeUtil() {
|
||||
}
|
||||
|
||||
public static long parseMillis(String input, long fallbackMillis) {
|
||||
if (input == null || input.isBlank()) {
|
||||
return fallbackMillis;
|
||||
}
|
||||
String normalized = input.trim().toLowerCase(Locale.ROOT);
|
||||
if (normalized.equals("0") || normalized.equals("none") || normalized.equals("off") || normalized.equals("disabled")) {
|
||||
return 0L;
|
||||
}
|
||||
if (normalized.matches("\\d+")) {
|
||||
return Long.parseLong(normalized) * 1000L;
|
||||
}
|
||||
|
||||
Matcher matcher = TOKEN.matcher(normalized.replace(" ", ""));
|
||||
double total = 0.0;
|
||||
int matches = 0;
|
||||
while (matcher.find()) {
|
||||
double value = Double.parseDouble(matcher.group(1));
|
||||
String unit = matcher.group(2).toLowerCase(Locale.ROOT);
|
||||
total += switch (unit) {
|
||||
case "ms" -> value;
|
||||
case "s" -> value * 1000.0;
|
||||
case "m" -> value * 60_000.0;
|
||||
case "h" -> value * 3_600_000.0;
|
||||
case "d" -> value * 86_400_000.0;
|
||||
case "w" -> value * 604_800_000.0;
|
||||
default -> 0.0;
|
||||
};
|
||||
matches++;
|
||||
}
|
||||
return matches == 0 ? fallbackMillis : Math.max(0L, Math.round(total));
|
||||
}
|
||||
|
||||
public static String formatDuration(long millis) {
|
||||
if (millis <= 0L) {
|
||||
return "never";
|
||||
}
|
||||
Duration duration = Duration.ofMillis(millis);
|
||||
long days = duration.toDays();
|
||||
duration = duration.minusDays(days);
|
||||
long hours = duration.toHours();
|
||||
duration = duration.minusHours(hours);
|
||||
long minutes = duration.toMinutes();
|
||||
duration = duration.minusMinutes(minutes);
|
||||
long seconds = Math.max(0L, duration.toSeconds());
|
||||
|
||||
if (days > 0) {
|
||||
return days + "d " + hours + "h";
|
||||
}
|
||||
if (hours > 0) {
|
||||
return hours + "h " + minutes + "m";
|
||||
}
|
||||
if (minutes > 0) {
|
||||
return minutes + "m " + seconds + "s";
|
||||
}
|
||||
return seconds + "s";
|
||||
}
|
||||
|
||||
public static String formatTimestamp(long epochMillis) {
|
||||
if (epochMillis <= 0L) {
|
||||
return "never";
|
||||
}
|
||||
return java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
|
||||
.withZone(java.time.ZoneId.systemDefault())
|
||||
.format(java.time.Instant.ofEpochMilli(epochMillis));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package com.dirtbagmc.dirtbounties.webhook;
|
||||
|
||||
import com.dirtbagmc.dirtbounties.config.ConfigService;
|
||||
import com.dirtbagmc.dirtbounties.service.MessageService;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import java.io.IOException;
|
||||
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.logging.Level;
|
||||
|
||||
public final class WebhookService {
|
||||
private final JavaPlugin plugin;
|
||||
private final ConfigService configService;
|
||||
private final MessageService messages;
|
||||
private HttpClient client;
|
||||
|
||||
public WebhookService(JavaPlugin plugin, ConfigService configService, MessageService messages) {
|
||||
this.plugin = plugin;
|
||||
this.configService = configService;
|
||||
this.messages = messages;
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
client = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(Math.max(1, configService.main().getInt("webhooks.timeout-seconds", 8))))
|
||||
.build();
|
||||
}
|
||||
|
||||
public void placement(String placer, String target, double total, String reason, boolean increased) {
|
||||
if (!configService.main().getBoolean("webhooks.notify-placements", true)
|
||||
|| total < configService.main().getDouble("webhooks.large-bounty-threshold", 25_000.0)) {
|
||||
return;
|
||||
}
|
||||
String title = messages.raw("webhook.placement-title", "New bounty placed");
|
||||
String content = title + "\n" + placer + (increased ? " increased " : " placed ")
|
||||
+ "a bounty on " + target + " worth " + total + ". Reason: " + reason;
|
||||
send(content);
|
||||
}
|
||||
|
||||
public void claim(String killer, String target, double payout) {
|
||||
if (!configService.main().getBoolean("webhooks.notify-claims", true)
|
||||
|| payout < configService.main().getDouble("webhooks.large-claim-threshold", 25_000.0)) {
|
||||
return;
|
||||
}
|
||||
String title = messages.raw("webhook.claim-title", "Bounty claimed");
|
||||
send(title + "\n" + killer + " claimed " + payout + " for killing " + target + ".");
|
||||
}
|
||||
|
||||
public void admin(String actor, String action) {
|
||||
if (!configService.main().getBoolean("webhooks.notify-admin-actions", true)) {
|
||||
return;
|
||||
}
|
||||
String title = messages.raw("webhook.admin-title", "Bounty admin action");
|
||||
send(title + "\n" + actor + ": " + action);
|
||||
}
|
||||
|
||||
private void send(String content) {
|
||||
if (!configService.main().getBoolean("webhooks.enabled", false)) {
|
||||
return;
|
||||
}
|
||||
String url = configService.main().getString("webhooks.url", "");
|
||||
if (url == null || url.isBlank()) {
|
||||
return;
|
||||
}
|
||||
String username = configService.main().getString("webhooks.username", "DirtBounties");
|
||||
String body = "{\"username\":\"" + escape(username) + "\",\"content\":\"" + escape(content) + "\"}";
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
try {
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(url))
|
||||
.timeout(Duration.ofSeconds(Math.max(1, configService.main().getInt("webhooks.timeout-seconds", 8))))
|
||||
.header("Content-Type", "application/json")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(body))
|
||||
.build();
|
||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
if (response.statusCode() >= 400) {
|
||||
plugin.getLogger().warning("Webhook returned HTTP " + response.statusCode());
|
||||
}
|
||||
} catch (IllegalArgumentException | IOException | InterruptedException ex) {
|
||||
if (ex instanceof InterruptedException) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
plugin.getLogger().log(Level.WARNING, "Could not send DirtBounties webhook.", ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String escape(String input) {
|
||||
return (input == null ? "" : input)
|
||||
.replace("\\", "\\\\")
|
||||
.replace("\"", "\\\"")
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
# DirtBounties main configuration
|
||||
# Paper 1.21.x, Java 21
|
||||
#
|
||||
# Color formatting supports normal ampersand colors, hex colors like &#D4AF37,
|
||||
# and the DirtbagMC gradient style shown below.
|
||||
|
||||
server:
|
||||
brand-name: "DirtbagMC"
|
||||
brand-gradient: "A2416&lᴅA2D1B&lɪA351F&lʀA3F24&lᴛA4A2A&lʙB6B35&lᴀ&#A8873F&lɢ&#D4AF37&lᴍ&#B9C63F&lᴄ"
|
||||
debug: false
|
||||
|
||||
storage:
|
||||
# Active bounty data is saved after important mutations and on this autosave interval.
|
||||
autosave-interval: "5m"
|
||||
cleanup-expired-interval: "10m"
|
||||
save-player-ip-cache: true
|
||||
write-history-to-disk-immediately: true
|
||||
|
||||
economy:
|
||||
enabled: true
|
||||
# DirtBounties uses Bukkit ServicesManager economy registration.
|
||||
# This supports normal Vault and CMI's Vault injector.
|
||||
fail-if-missing: false
|
||||
provider-log-on-enable: true
|
||||
minimum-balance-after-withdraw: 0.0
|
||||
currency-format: "${amount}"
|
||||
fees:
|
||||
# Placement fee can be charged as extra money or deducted from the bounty value.
|
||||
placement-percent: 5.0
|
||||
placement-mode: "extra" # extra, deduct
|
||||
add-percent: 5.0
|
||||
add-mode: "extra" # extra, deduct
|
||||
claim-tax-percent: 0.0
|
||||
claim-sink-percent: 0.0
|
||||
broadcasts:
|
||||
enabled: true
|
||||
placed-threshold: 5000.0
|
||||
claimed-threshold: 5000.0
|
||||
milestone-thresholds:
|
||||
- 10000.0
|
||||
- 25000.0
|
||||
- 50000.0
|
||||
|
||||
bounties:
|
||||
min-amount: 100.0
|
||||
max-amount: 1000000.0
|
||||
stack-existing: true
|
||||
allow-self-target: false
|
||||
allow-offline-targets: true
|
||||
# If false, targets must be online or have joined before.
|
||||
# If true, Bukkit may create an OfflinePlayer profile for unknown names.
|
||||
allow-never-joined-targets: false
|
||||
allow-banned-targets: false
|
||||
allow-anonymous: true
|
||||
anonymous-requires-permission: true
|
||||
allow-reasons: true
|
||||
max-reason-length: 80
|
||||
default-reason: "No reason given."
|
||||
placement-cooldown: "30s"
|
||||
require-confirmation-gui: true
|
||||
|
||||
expiration:
|
||||
enabled: true
|
||||
default-duration: "14d"
|
||||
max-duration: "30d"
|
||||
# If true, a bounty without contributions gets removed during cleanup.
|
||||
remove-empty-bounties: true
|
||||
banned-player-action: "keep" # keep, expire
|
||||
deleted-player-action: "keep" # keep, expire
|
||||
|
||||
refunds:
|
||||
# Refund percentage is based on the active contribution value, not placement fees.
|
||||
on-admin-remove-percent: 100.0
|
||||
on-expire-percent: 50.0
|
||||
on-invalid-percent: 100.0
|
||||
refund-fees: false
|
||||
refund-offline-players: true
|
||||
minimum-refund: 0.01
|
||||
|
||||
claim-rules:
|
||||
require-permission: true
|
||||
require-pvp-kill: true
|
||||
block-environmental-deaths: true
|
||||
prevent-self-claims: true
|
||||
prevent-same-ip-claims: true
|
||||
prevent-shared-known-ip-claims: true
|
||||
allow-bypass-permission: true
|
||||
require-target-online-at-death: true
|
||||
blocked-killer-game-modes:
|
||||
- CREATIVE
|
||||
- SPECTATOR
|
||||
worlds:
|
||||
mode: "blacklist" # disabled, whitelist, blacklist
|
||||
list:
|
||||
- spawn
|
||||
- events
|
||||
combat:
|
||||
enabled: true
|
||||
window: "30s"
|
||||
min-damage: 4.0
|
||||
min-hits: 1
|
||||
min-combat-duration: "0s"
|
||||
|
||||
anti-abuse:
|
||||
enabled: true
|
||||
log-failed-claims: true
|
||||
log-same-ip-attempts: true
|
||||
killer-claim-cooldown: "2m"
|
||||
target-claim-cooldown: "2m"
|
||||
killer-target-pair-cooldown: "12h"
|
||||
same-victim-cooldown: "10m"
|
||||
pair-window: "7d"
|
||||
max-pair-claims-in-window: 2
|
||||
suspicious-history-limit: 500
|
||||
run-console-commands-on-suspicious: false
|
||||
suspicious-commands:
|
||||
- "staffmsg Suspicious bounty claim: {killer} -> {target}: {reason}"
|
||||
|
||||
history:
|
||||
enabled: true
|
||||
max-records: 2000
|
||||
max-records-per-player-command: 10
|
||||
prune-older-than: "90d"
|
||||
|
||||
gui:
|
||||
enabled: true
|
||||
open-sound: "BLOCK_BARREL_OPEN"
|
||||
click-sound: "UI_BUTTON_CLICK"
|
||||
success-sound: "ENTITY_PLAYER_LEVELUP"
|
||||
error-sound: "ENTITY_VILLAGER_NO"
|
||||
items-per-page: 28
|
||||
refresh-after-action: true
|
||||
close-on-confirm: true
|
||||
chat-input-timeout: "60s"
|
||||
|
||||
webhooks:
|
||||
enabled: false
|
||||
url: ""
|
||||
username: "DirtBounties"
|
||||
large-bounty-threshold: 25000.0
|
||||
large-claim-threshold: 25000.0
|
||||
notify-placements: true
|
||||
notify-claims: true
|
||||
notify-admin-actions: true
|
||||
timeout-seconds: 8
|
||||
|
||||
logging:
|
||||
console:
|
||||
economy-status: true
|
||||
placements: true
|
||||
claims: true
|
||||
admin-actions: true
|
||||
suspicious: true
|
||||
file-history: true
|
||||
@@ -0,0 +1,177 @@
|
||||
titles:
|
||||
main: "A2416&lDirtBounties &8| &6Active"
|
||||
top: "A2416&lDirtBounties &8| &6Top"
|
||||
detail: "A2416&lDirtBounties &8| &c{target}"
|
||||
confirm: "A2416&lDirtBounties &8| &aConfirm"
|
||||
admin-main: "A2416&lDirtBounties &8| &4Admin"
|
||||
admin-history: "A2416&lDirtBounties &8| &4History"
|
||||
admin-suspicious: "A2416&lDirtBounties &8| &4Suspicious"
|
||||
|
||||
layout:
|
||||
size: 54
|
||||
content-slots:
|
||||
- 10
|
||||
- 11
|
||||
- 12
|
||||
- 13
|
||||
- 14
|
||||
- 15
|
||||
- 16
|
||||
- 19
|
||||
- 20
|
||||
- 21
|
||||
- 22
|
||||
- 23
|
||||
- 24
|
||||
- 25
|
||||
- 28
|
||||
- 29
|
||||
- 30
|
||||
- 31
|
||||
- 32
|
||||
- 33
|
||||
- 34
|
||||
- 37
|
||||
- 38
|
||||
- 39
|
||||
- 40
|
||||
- 41
|
||||
- 42
|
||||
- 43
|
||||
previous-slot: 45
|
||||
back-slot: 46
|
||||
refresh-slot: 49
|
||||
next-slot: 53
|
||||
place-slot: 48
|
||||
top-slot: 50
|
||||
admin-slot: 52
|
||||
|
||||
items:
|
||||
filler:
|
||||
material: BLACK_STAINED_GLASS_PANE
|
||||
name: " "
|
||||
lore: []
|
||||
border:
|
||||
material: BROWN_STAINED_GLASS_PANE
|
||||
name: " "
|
||||
lore: []
|
||||
empty:
|
||||
material: BARRIER
|
||||
name: "&cNo bounties"
|
||||
lore:
|
||||
- "&7Nobody has a price on their head."
|
||||
bounty:
|
||||
material: PLAYER_HEAD
|
||||
name: "&c&l{target}"
|
||||
lore:
|
||||
- "&7Bounty: &f{amount}"
|
||||
- "&7Contributors: &f{contributors}"
|
||||
- "&7Top reason: &f{reason}"
|
||||
- "&7Expires: &f{expires}"
|
||||
- ""
|
||||
- "&eClick to view details."
|
||||
top-bounty:
|
||||
material: PLAYER_HEAD
|
||||
name: "&6#{rank} &c&l{target}"
|
||||
lore:
|
||||
- "&7Bounty: &f{amount}"
|
||||
- "&7Contributors: &f{contributors}"
|
||||
- ""
|
||||
- "&eClick to inspect."
|
||||
detail-head:
|
||||
material: PLAYER_HEAD
|
||||
name: "&c&l{target}"
|
||||
lore:
|
||||
- "&7Total bounty: &f{amount}"
|
||||
- "&7Contributors: &f{contributors}"
|
||||
- "&7Expires: &f{expires}"
|
||||
- ""
|
||||
- "&6Top reasons:"
|
||||
- "{reason_lines}"
|
||||
claim-info:
|
||||
material: BOOK
|
||||
name: "&6Claim Conditions"
|
||||
lore:
|
||||
- "&7PvP required: &f{pvp}"
|
||||
- "&7Same IP blocked: &f{same_ip}"
|
||||
- "&7World mode: &f{world_mode}"
|
||||
- "&7Combat: &f{combat}"
|
||||
- ""
|
||||
- "&8Claims are checked automatically on kill."
|
||||
place:
|
||||
material: GOLD_INGOT
|
||||
name: "&6Place Bounty"
|
||||
lore:
|
||||
- "&7Start a guided bounty placement."
|
||||
- ""
|
||||
- "&eClick to begin."
|
||||
add:
|
||||
material: ANVIL
|
||||
name: "&6Increase Bounty"
|
||||
lore:
|
||||
- "&7Add money to this target's bounty."
|
||||
- ""
|
||||
- "&eClick to continue."
|
||||
top:
|
||||
material: NETHER_STAR
|
||||
name: "&6Top Bounties"
|
||||
lore:
|
||||
- "&7Sort by highest active value."
|
||||
- ""
|
||||
- "&eClick to view."
|
||||
refresh:
|
||||
material: SUNFLOWER
|
||||
name: "&eRefresh"
|
||||
lore:
|
||||
- "&7Reload this view."
|
||||
previous:
|
||||
material: ARROW
|
||||
name: "&ePrevious Page"
|
||||
lore:
|
||||
- "&7Go back one page."
|
||||
next:
|
||||
material: ARROW
|
||||
name: "&eNext Page"
|
||||
lore:
|
||||
- "&7Go forward one page."
|
||||
back:
|
||||
material: OAK_DOOR
|
||||
name: "&eBack"
|
||||
lore:
|
||||
- "&7Return to the previous view."
|
||||
close:
|
||||
material: BARRIER
|
||||
name: "&cClose"
|
||||
lore:
|
||||
- "&7Close this menu."
|
||||
confirm:
|
||||
material: LIME_CONCRETE
|
||||
name: "&aConfirm Bounty"
|
||||
lore:
|
||||
- "&7Target: &f{target}"
|
||||
- "&7Amount: &f{amount}"
|
||||
- "&7Fee: &f{fee}"
|
||||
- "&7Total cost: &f{cost}"
|
||||
- "&7Reason: &f{reason}"
|
||||
- ""
|
||||
- "&aClick to confirm."
|
||||
cancel:
|
||||
material: RED_CONCRETE
|
||||
name: "&cCancel"
|
||||
lore:
|
||||
- "&7Return without placing this bounty."
|
||||
admin-active:
|
||||
material: CHEST
|
||||
name: "&4Active Bounties"
|
||||
lore:
|
||||
- "&7Review and inspect all active bounties."
|
||||
admin-history:
|
||||
material: WRITABLE_BOOK
|
||||
name: "&4Recent Claims and Changes"
|
||||
lore:
|
||||
- "&7Review bounty history records."
|
||||
admin-suspicious:
|
||||
material: REDSTONE_TORCH
|
||||
name: "&4Suspicious Activity"
|
||||
lore:
|
||||
- "&7Review blocked and suspicious claims."
|
||||
@@ -0,0 +1,105 @@
|
||||
prefix: "A2416&lᴅA2D1B&lɪA351F&lʀA3F24&lᴛA4A2A&lʙB6B35&lᴀ&#A8873F&lɢ&#D4AF37&lᴍ&#B9C63F&lᴄ &8» "
|
||||
no-permission: "{prefix}&cYou do not have permission to do that."
|
||||
player-only: "{prefix}&cOnly players can use that command."
|
||||
unknown-command: "{prefix}&cUnknown bounty command. Use &f/bounty help&c."
|
||||
reload-complete: "{prefix}&aDirtBounties reloaded."
|
||||
invalid-number: "{prefix}&cThat amount is not valid."
|
||||
invalid-player: "{prefix}&cCould not find that player."
|
||||
invalid-world: "{prefix}&cBounties cannot be claimed in this world."
|
||||
economy-missing: "{prefix}&cEconomy is not available. Ask staff to check Vault or CMI's Vault injector."
|
||||
not-enough-money: "{prefix}&cYou need &f{cost}&c, including fees, to place that bounty."
|
||||
cooldown: "{prefix}&cSlow down. Try again in &f{time}&c."
|
||||
reason-too-long: "{prefix}&cThat reason is too long. Maximum: &f{max}&c characters."
|
||||
target-self: "{prefix}&cYou cannot place or claim a bounty on yourself."
|
||||
target-banned: "{prefix}&cThat player cannot receive bounties while banned."
|
||||
amount-too-low: "{prefix}&cMinimum bounty amount is &f{min}&c."
|
||||
amount-too-high: "{prefix}&cMaximum bounty amount is &f{max}&c."
|
||||
amount-would-exceed-max: "{prefix}&cThat would put the bounty above the maximum of &f{max}&c."
|
||||
no-active-bounties: "{prefix}&7There are no active bounties right now."
|
||||
bounty-not-found: "{prefix}&cThere is no active bounty on &f{target}&c."
|
||||
claim-denied-format: "{prefix}&cBounty claim denied: &f{reason}"
|
||||
|
||||
help:
|
||||
- "{prefix}&6&lDirtBounties Commands"
|
||||
- "&8- &e/bounty &7Open the bounty GUI."
|
||||
- "&8- &e/bounty place <player> <amount> [reason] &7Place a bounty."
|
||||
- "&8- &e/bounty add <player> <amount> [reason] &7Increase a bounty."
|
||||
- "&8- &e/bounty list &7List active bounties."
|
||||
- "&8- &e/bounty top &7View top bounties."
|
||||
- "&8- &e/bounty view <player> &7View bounty details."
|
||||
- "&8- &e/bounty claiminfo <player> &7View claim rules."
|
||||
|
||||
admin-help:
|
||||
- "{prefix}&6&lDirtBounties Admin"
|
||||
- "&8- &e/bountyadmin reload &7Reload configs and storage."
|
||||
- "&8- &e/bountyadmin remove <player> &7Remove a bounty and refund by config."
|
||||
- "&8- &e/bountyadmin clearall confirm &7Remove every active bounty."
|
||||
- "&8- &e/bountyadmin set <player> <amount> &7Set a bounty value."
|
||||
- "&8- &e/bountyadmin expire <player> &7Expire a bounty."
|
||||
- "&8- &e/bountyadmin history <player> &7Show history."
|
||||
- "&8- &e/bountyadmin suspicious &7Show suspicious activity."
|
||||
- "&8- &e/bountyadmin gui &7Open admin GUI."
|
||||
|
||||
bounty:
|
||||
placed: "{prefix}&aPlaced a bounty of &f{amount}&a on &f{target}&a. Fee: &f{fee}&a."
|
||||
added: "{prefix}&aAdded &f{amount}&a to &f{target}&a's bounty. New total: &f{total}&a."
|
||||
broadcast-placed: "{prefix}&6{placer}&e placed a bounty of &f{amount}&e on &c{target}&e."
|
||||
broadcast-added: "{prefix}&6{placer}&e increased &c{target}&e's bounty to &f{total}&e."
|
||||
milestone: "{prefix}&c{target}&6's bounty has reached &f{total}&6."
|
||||
claimed: "{prefix}&aYou claimed &f{payout}&a from &c{target}&a's bounty."
|
||||
claim-broadcast: "{prefix}&c{killer}&6 claimed &f{payout}&6 for killing &c{target}&6."
|
||||
expired: "{prefix}&7The bounty on &f{target}&7 expired."
|
||||
removed: "{prefix}&aRemoved the bounty on &f{target}&a."
|
||||
set: "{prefix}&aSet &f{target}&a's bounty to &f{amount}&a."
|
||||
clearall-warning: "{prefix}&cUse &f/bountyadmin clearall confirm &cto remove all active bounties."
|
||||
clearall-done: "{prefix}&aRemoved &f{count}&a active bounties."
|
||||
list-line: "&8- &c{target} &7» &f{amount} &8({contributors} contributors)"
|
||||
top-line: "&6#{rank} &c{target} &7» &f{amount}"
|
||||
view:
|
||||
- "{prefix}&6&lBounty: &c{target}"
|
||||
- "&7Amount: &f{amount}"
|
||||
- "&7Contributors: &f{contributors}"
|
||||
- "&7Expires: &f{expires}"
|
||||
- "&7Reason: &f{reason}"
|
||||
claiminfo:
|
||||
- "{prefix}&6&lClaim Rules for &c{target}"
|
||||
- "&8- &7PvP kill required: &f{pvp}"
|
||||
- "&8- &7Same-IP claims blocked: &f{same_ip}"
|
||||
- "&8- &7Allowed worlds: &f{worlds}"
|
||||
- "&8- &7Combat requirement: &f{combat}"
|
||||
|
||||
claim-denied:
|
||||
no-bounty: "No active bounty exists."
|
||||
no-permission: "You do not have permission to claim bounties."
|
||||
self: "Self-claims are blocked."
|
||||
same-ip: "Same-IP bounty claims are blocked."
|
||||
shared-known-ip: "Shared known-IP bounty claims are blocked."
|
||||
world: "Claims are disabled in this world."
|
||||
gamemode: "Your game mode cannot claim bounties."
|
||||
combat: "Combat requirements were not met."
|
||||
cooldown: "A claim cooldown is active."
|
||||
pair-limit: "This killer-target pair has too many recent claims."
|
||||
target-offline: "The target must be online at death."
|
||||
|
||||
gui:
|
||||
unavailable: "{prefix}&cThe bounty GUI is disabled."
|
||||
prompt-target: "{prefix}&eType the target player's name in chat, or type &fcancel&e."
|
||||
prompt-amount: "{prefix}&eType the bounty amount for &f{target}&e, or type &fcancel&e."
|
||||
prompt-reason: "{prefix}&eType a short reason for &f{target}&e, use &f-&e for none, or type &fcancel&e."
|
||||
input-cancelled: "{prefix}&7Bounty input cancelled."
|
||||
input-expired: "{prefix}&cYour bounty input expired."
|
||||
confirm-opened: "{prefix}&7Review and confirm the bounty."
|
||||
admin-opened: "{prefix}&7Opened the admin bounty view."
|
||||
|
||||
admin:
|
||||
history-empty: "{prefix}&7No bounty history found for &f{target}&7."
|
||||
suspicious-empty: "{prefix}&7No suspicious bounty activity has been logged."
|
||||
suspicious-line: "&8- &c{time} &7{type}: &f{details}"
|
||||
history-line: "&8- &e{time} &7{type}: &f{amount} &8({note})"
|
||||
refunded: "{prefix}&aRefunded &f{amount}&a to &f{player}&a."
|
||||
action-logged: "{prefix}&7Admin action logged."
|
||||
|
||||
webhook:
|
||||
placement-title: "New bounty placed"
|
||||
claim-title: "Bounty claimed"
|
||||
admin-title: "Bounty admin action"
|
||||
@@ -0,0 +1,67 @@
|
||||
name: DirtBounties
|
||||
version: 1.0.0
|
||||
main: com.dirtbagmc.dirtbounties.DirtBountiesPlugin
|
||||
api-version: '1.21'
|
||||
author: DirtbagMC
|
||||
website: https://dirtbagmc.com
|
||||
description: Premium bounty system for Paper SMP and anarchy servers.
|
||||
softdepend:
|
||||
- Vault
|
||||
- CMI
|
||||
- PlaceholderAPI
|
||||
|
||||
commands:
|
||||
bounty:
|
||||
description: Open and manage player bounties.
|
||||
usage: /bounty help
|
||||
aliases:
|
||||
- bounties
|
||||
- dbounty
|
||||
bountyadmin:
|
||||
description: Admin controls for DirtBounties.
|
||||
usage: /bountyadmin help
|
||||
aliases:
|
||||
- dbountyadmin
|
||||
- ba
|
||||
|
||||
permissions:
|
||||
dirtbounties.use:
|
||||
description: Allows opening the bounty GUI and using basic commands.
|
||||
default: true
|
||||
dirtbounties.place:
|
||||
description: Allows placing new bounties.
|
||||
default: true
|
||||
dirtbounties.add:
|
||||
description: Allows increasing existing bounties.
|
||||
default: true
|
||||
dirtbounties.view:
|
||||
description: Allows viewing bounty details.
|
||||
default: true
|
||||
dirtbounties.top:
|
||||
description: Allows viewing top bounties.
|
||||
default: true
|
||||
dirtbounties.claim:
|
||||
description: Allows claiming bounties by killing targets.
|
||||
default: true
|
||||
dirtbounties.anonymous:
|
||||
description: Allows placing anonymous bounties when enabled.
|
||||
default: op
|
||||
dirtbounties.bypass.cooldowns:
|
||||
description: Bypasses placement and claim cooldown checks.
|
||||
default: op
|
||||
dirtbounties.bypass.claimrules:
|
||||
description: Bypasses configured claim restrictions.
|
||||
default: op
|
||||
dirtbounties.admin:
|
||||
description: Full DirtBounties administration access.
|
||||
default: op
|
||||
children:
|
||||
dirtbounties.use: true
|
||||
dirtbounties.place: true
|
||||
dirtbounties.add: true
|
||||
dirtbounties.view: true
|
||||
dirtbounties.top: true
|
||||
dirtbounties.claim: true
|
||||
dirtbounties.anonymous: true
|
||||
dirtbounties.bypass.cooldowns: true
|
||||
dirtbounties.bypass.claimrules: true
|
||||
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.
BIN
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,154 @@
|
||||
# DirtBounties main configuration
|
||||
# Paper 1.21.x, Java 21
|
||||
#
|
||||
# Color formatting supports normal ampersand colors, hex colors like &#D4AF37,
|
||||
# and the DirtbagMC gradient style shown below.
|
||||
|
||||
server:
|
||||
brand-name: "DirtbagMC"
|
||||
brand-gradient: "A2416&lᴅA2D1B&lɪA351F&lʀA3F24&lᴛA4A2A&lʙB6B35&lᴀ&#A8873F&lɢ&#D4AF37&lᴍ&#B9C63F&lᴄ"
|
||||
debug: false
|
||||
|
||||
storage:
|
||||
# Active bounty data is saved after important mutations and on this autosave interval.
|
||||
autosave-interval: "5m"
|
||||
cleanup-expired-interval: "10m"
|
||||
save-player-ip-cache: true
|
||||
write-history-to-disk-immediately: true
|
||||
|
||||
economy:
|
||||
enabled: true
|
||||
# DirtBounties uses Bukkit ServicesManager economy registration.
|
||||
# This supports normal Vault and CMI's Vault injector.
|
||||
fail-if-missing: false
|
||||
provider-log-on-enable: true
|
||||
minimum-balance-after-withdraw: 0.0
|
||||
currency-format: "${amount}"
|
||||
fees:
|
||||
# Placement fee can be charged as extra money or deducted from the bounty value.
|
||||
placement-percent: 5.0
|
||||
placement-mode: "extra" # extra, deduct
|
||||
add-percent: 5.0
|
||||
add-mode: "extra" # extra, deduct
|
||||
claim-tax-percent: 0.0
|
||||
claim-sink-percent: 0.0
|
||||
broadcasts:
|
||||
enabled: true
|
||||
placed-threshold: 5000.0
|
||||
claimed-threshold: 5000.0
|
||||
milestone-thresholds:
|
||||
- 10000.0
|
||||
- 25000.0
|
||||
- 50000.0
|
||||
|
||||
bounties:
|
||||
min-amount: 100.0
|
||||
max-amount: 1000000.0
|
||||
stack-existing: true
|
||||
allow-self-target: false
|
||||
allow-offline-targets: true
|
||||
# If false, targets must be online or have joined before.
|
||||
# If true, Bukkit may create an OfflinePlayer profile for unknown names.
|
||||
allow-never-joined-targets: false
|
||||
allow-banned-targets: false
|
||||
allow-anonymous: true
|
||||
anonymous-requires-permission: true
|
||||
allow-reasons: true
|
||||
max-reason-length: 80
|
||||
default-reason: "No reason given."
|
||||
placement-cooldown: "30s"
|
||||
require-confirmation-gui: true
|
||||
|
||||
expiration:
|
||||
enabled: true
|
||||
default-duration: "14d"
|
||||
max-duration: "30d"
|
||||
# If true, a bounty without contributions gets removed during cleanup.
|
||||
remove-empty-bounties: true
|
||||
banned-player-action: "keep" # keep, expire
|
||||
deleted-player-action: "keep" # keep, expire
|
||||
|
||||
refunds:
|
||||
# Refund percentage is based on the active contribution value, not placement fees.
|
||||
on-admin-remove-percent: 100.0
|
||||
on-expire-percent: 50.0
|
||||
on-invalid-percent: 100.0
|
||||
refund-fees: false
|
||||
refund-offline-players: true
|
||||
minimum-refund: 0.01
|
||||
|
||||
claim-rules:
|
||||
require-permission: true
|
||||
require-pvp-kill: true
|
||||
block-environmental-deaths: true
|
||||
prevent-self-claims: true
|
||||
prevent-same-ip-claims: true
|
||||
prevent-shared-known-ip-claims: true
|
||||
allow-bypass-permission: true
|
||||
require-target-online-at-death: true
|
||||
blocked-killer-game-modes:
|
||||
- CREATIVE
|
||||
- SPECTATOR
|
||||
worlds:
|
||||
mode: "blacklist" # disabled, whitelist, blacklist
|
||||
list:
|
||||
- spawn
|
||||
- events
|
||||
combat:
|
||||
enabled: true
|
||||
window: "30s"
|
||||
min-damage: 4.0
|
||||
min-hits: 1
|
||||
min-combat-duration: "0s"
|
||||
|
||||
anti-abuse:
|
||||
enabled: true
|
||||
log-failed-claims: true
|
||||
log-same-ip-attempts: true
|
||||
killer-claim-cooldown: "2m"
|
||||
target-claim-cooldown: "2m"
|
||||
killer-target-pair-cooldown: "12h"
|
||||
same-victim-cooldown: "10m"
|
||||
pair-window: "7d"
|
||||
max-pair-claims-in-window: 2
|
||||
suspicious-history-limit: 500
|
||||
run-console-commands-on-suspicious: false
|
||||
suspicious-commands:
|
||||
- "staffmsg Suspicious bounty claim: {killer} -> {target}: {reason}"
|
||||
|
||||
history:
|
||||
enabled: true
|
||||
max-records: 2000
|
||||
max-records-per-player-command: 10
|
||||
prune-older-than: "90d"
|
||||
|
||||
gui:
|
||||
enabled: true
|
||||
open-sound: "BLOCK_BARREL_OPEN"
|
||||
click-sound: "UI_BUTTON_CLICK"
|
||||
success-sound: "ENTITY_PLAYER_LEVELUP"
|
||||
error-sound: "ENTITY_VILLAGER_NO"
|
||||
items-per-page: 28
|
||||
refresh-after-action: true
|
||||
close-on-confirm: true
|
||||
chat-input-timeout: "60s"
|
||||
|
||||
webhooks:
|
||||
enabled: false
|
||||
url: ""
|
||||
username: "DirtBounties"
|
||||
large-bounty-threshold: 25000.0
|
||||
large-claim-threshold: 25000.0
|
||||
notify-placements: true
|
||||
notify-claims: true
|
||||
notify-admin-actions: true
|
||||
timeout-seconds: 8
|
||||
|
||||
logging:
|
||||
console:
|
||||
economy-status: true
|
||||
placements: true
|
||||
claims: true
|
||||
admin-actions: true
|
||||
suspicious: true
|
||||
file-history: true
|
||||
@@ -0,0 +1,177 @@
|
||||
titles:
|
||||
main: "A2416&lDirtBounties &8| &6Active"
|
||||
top: "A2416&lDirtBounties &8| &6Top"
|
||||
detail: "A2416&lDirtBounties &8| &c{target}"
|
||||
confirm: "A2416&lDirtBounties &8| &aConfirm"
|
||||
admin-main: "A2416&lDirtBounties &8| &4Admin"
|
||||
admin-history: "A2416&lDirtBounties &8| &4History"
|
||||
admin-suspicious: "A2416&lDirtBounties &8| &4Suspicious"
|
||||
|
||||
layout:
|
||||
size: 54
|
||||
content-slots:
|
||||
- 10
|
||||
- 11
|
||||
- 12
|
||||
- 13
|
||||
- 14
|
||||
- 15
|
||||
- 16
|
||||
- 19
|
||||
- 20
|
||||
- 21
|
||||
- 22
|
||||
- 23
|
||||
- 24
|
||||
- 25
|
||||
- 28
|
||||
- 29
|
||||
- 30
|
||||
- 31
|
||||
- 32
|
||||
- 33
|
||||
- 34
|
||||
- 37
|
||||
- 38
|
||||
- 39
|
||||
- 40
|
||||
- 41
|
||||
- 42
|
||||
- 43
|
||||
previous-slot: 45
|
||||
back-slot: 46
|
||||
refresh-slot: 49
|
||||
next-slot: 53
|
||||
place-slot: 48
|
||||
top-slot: 50
|
||||
admin-slot: 52
|
||||
|
||||
items:
|
||||
filler:
|
||||
material: BLACK_STAINED_GLASS_PANE
|
||||
name: " "
|
||||
lore: []
|
||||
border:
|
||||
material: BROWN_STAINED_GLASS_PANE
|
||||
name: " "
|
||||
lore: []
|
||||
empty:
|
||||
material: BARRIER
|
||||
name: "&cNo bounties"
|
||||
lore:
|
||||
- "&7Nobody has a price on their head."
|
||||
bounty:
|
||||
material: PLAYER_HEAD
|
||||
name: "&c&l{target}"
|
||||
lore:
|
||||
- "&7Bounty: &f{amount}"
|
||||
- "&7Contributors: &f{contributors}"
|
||||
- "&7Top reason: &f{reason}"
|
||||
- "&7Expires: &f{expires}"
|
||||
- ""
|
||||
- "&eClick to view details."
|
||||
top-bounty:
|
||||
material: PLAYER_HEAD
|
||||
name: "&6#{rank} &c&l{target}"
|
||||
lore:
|
||||
- "&7Bounty: &f{amount}"
|
||||
- "&7Contributors: &f{contributors}"
|
||||
- ""
|
||||
- "&eClick to inspect."
|
||||
detail-head:
|
||||
material: PLAYER_HEAD
|
||||
name: "&c&l{target}"
|
||||
lore:
|
||||
- "&7Total bounty: &f{amount}"
|
||||
- "&7Contributors: &f{contributors}"
|
||||
- "&7Expires: &f{expires}"
|
||||
- ""
|
||||
- "&6Top reasons:"
|
||||
- "{reason_lines}"
|
||||
claim-info:
|
||||
material: BOOK
|
||||
name: "&6Claim Conditions"
|
||||
lore:
|
||||
- "&7PvP required: &f{pvp}"
|
||||
- "&7Same IP blocked: &f{same_ip}"
|
||||
- "&7World mode: &f{world_mode}"
|
||||
- "&7Combat: &f{combat}"
|
||||
- ""
|
||||
- "&8Claims are checked automatically on kill."
|
||||
place:
|
||||
material: GOLD_INGOT
|
||||
name: "&6Place Bounty"
|
||||
lore:
|
||||
- "&7Start a guided bounty placement."
|
||||
- ""
|
||||
- "&eClick to begin."
|
||||
add:
|
||||
material: ANVIL
|
||||
name: "&6Increase Bounty"
|
||||
lore:
|
||||
- "&7Add money to this target's bounty."
|
||||
- ""
|
||||
- "&eClick to continue."
|
||||
top:
|
||||
material: NETHER_STAR
|
||||
name: "&6Top Bounties"
|
||||
lore:
|
||||
- "&7Sort by highest active value."
|
||||
- ""
|
||||
- "&eClick to view."
|
||||
refresh:
|
||||
material: SUNFLOWER
|
||||
name: "&eRefresh"
|
||||
lore:
|
||||
- "&7Reload this view."
|
||||
previous:
|
||||
material: ARROW
|
||||
name: "&ePrevious Page"
|
||||
lore:
|
||||
- "&7Go back one page."
|
||||
next:
|
||||
material: ARROW
|
||||
name: "&eNext Page"
|
||||
lore:
|
||||
- "&7Go forward one page."
|
||||
back:
|
||||
material: OAK_DOOR
|
||||
name: "&eBack"
|
||||
lore:
|
||||
- "&7Return to the previous view."
|
||||
close:
|
||||
material: BARRIER
|
||||
name: "&cClose"
|
||||
lore:
|
||||
- "&7Close this menu."
|
||||
confirm:
|
||||
material: LIME_CONCRETE
|
||||
name: "&aConfirm Bounty"
|
||||
lore:
|
||||
- "&7Target: &f{target}"
|
||||
- "&7Amount: &f{amount}"
|
||||
- "&7Fee: &f{fee}"
|
||||
- "&7Total cost: &f{cost}"
|
||||
- "&7Reason: &f{reason}"
|
||||
- ""
|
||||
- "&aClick to confirm."
|
||||
cancel:
|
||||
material: RED_CONCRETE
|
||||
name: "&cCancel"
|
||||
lore:
|
||||
- "&7Return without placing this bounty."
|
||||
admin-active:
|
||||
material: CHEST
|
||||
name: "&4Active Bounties"
|
||||
lore:
|
||||
- "&7Review and inspect all active bounties."
|
||||
admin-history:
|
||||
material: WRITABLE_BOOK
|
||||
name: "&4Recent Claims and Changes"
|
||||
lore:
|
||||
- "&7Review bounty history records."
|
||||
admin-suspicious:
|
||||
material: REDSTONE_TORCH
|
||||
name: "&4Suspicious Activity"
|
||||
lore:
|
||||
- "&7Review blocked and suspicious claims."
|
||||
@@ -0,0 +1,105 @@
|
||||
prefix: "A2416&lᴅA2D1B&lɪA351F&lʀA3F24&lᴛA4A2A&lʙB6B35&lᴀ&#A8873F&lɢ&#D4AF37&lᴍ&#B9C63F&lᴄ &8» "
|
||||
no-permission: "{prefix}&cYou do not have permission to do that."
|
||||
player-only: "{prefix}&cOnly players can use that command."
|
||||
unknown-command: "{prefix}&cUnknown bounty command. Use &f/bounty help&c."
|
||||
reload-complete: "{prefix}&aDirtBounties reloaded."
|
||||
invalid-number: "{prefix}&cThat amount is not valid."
|
||||
invalid-player: "{prefix}&cCould not find that player."
|
||||
invalid-world: "{prefix}&cBounties cannot be claimed in this world."
|
||||
economy-missing: "{prefix}&cEconomy is not available. Ask staff to check Vault or CMI's Vault injector."
|
||||
not-enough-money: "{prefix}&cYou need &f{cost}&c, including fees, to place that bounty."
|
||||
cooldown: "{prefix}&cSlow down. Try again in &f{time}&c."
|
||||
reason-too-long: "{prefix}&cThat reason is too long. Maximum: &f{max}&c characters."
|
||||
target-self: "{prefix}&cYou cannot place or claim a bounty on yourself."
|
||||
target-banned: "{prefix}&cThat player cannot receive bounties while banned."
|
||||
amount-too-low: "{prefix}&cMinimum bounty amount is &f{min}&c."
|
||||
amount-too-high: "{prefix}&cMaximum bounty amount is &f{max}&c."
|
||||
amount-would-exceed-max: "{prefix}&cThat would put the bounty above the maximum of &f{max}&c."
|
||||
no-active-bounties: "{prefix}&7There are no active bounties right now."
|
||||
bounty-not-found: "{prefix}&cThere is no active bounty on &f{target}&c."
|
||||
claim-denied-format: "{prefix}&cBounty claim denied: &f{reason}"
|
||||
|
||||
help:
|
||||
- "{prefix}&6&lDirtBounties Commands"
|
||||
- "&8- &e/bounty &7Open the bounty GUI."
|
||||
- "&8- &e/bounty place <player> <amount> [reason] &7Place a bounty."
|
||||
- "&8- &e/bounty add <player> <amount> [reason] &7Increase a bounty."
|
||||
- "&8- &e/bounty list &7List active bounties."
|
||||
- "&8- &e/bounty top &7View top bounties."
|
||||
- "&8- &e/bounty view <player> &7View bounty details."
|
||||
- "&8- &e/bounty claiminfo <player> &7View claim rules."
|
||||
|
||||
admin-help:
|
||||
- "{prefix}&6&lDirtBounties Admin"
|
||||
- "&8- &e/bountyadmin reload &7Reload configs and storage."
|
||||
- "&8- &e/bountyadmin remove <player> &7Remove a bounty and refund by config."
|
||||
- "&8- &e/bountyadmin clearall confirm &7Remove every active bounty."
|
||||
- "&8- &e/bountyadmin set <player> <amount> &7Set a bounty value."
|
||||
- "&8- &e/bountyadmin expire <player> &7Expire a bounty."
|
||||
- "&8- &e/bountyadmin history <player> &7Show history."
|
||||
- "&8- &e/bountyadmin suspicious &7Show suspicious activity."
|
||||
- "&8- &e/bountyadmin gui &7Open admin GUI."
|
||||
|
||||
bounty:
|
||||
placed: "{prefix}&aPlaced a bounty of &f{amount}&a on &f{target}&a. Fee: &f{fee}&a."
|
||||
added: "{prefix}&aAdded &f{amount}&a to &f{target}&a's bounty. New total: &f{total}&a."
|
||||
broadcast-placed: "{prefix}&6{placer}&e placed a bounty of &f{amount}&e on &c{target}&e."
|
||||
broadcast-added: "{prefix}&6{placer}&e increased &c{target}&e's bounty to &f{total}&e."
|
||||
milestone: "{prefix}&c{target}&6's bounty has reached &f{total}&6."
|
||||
claimed: "{prefix}&aYou claimed &f{payout}&a from &c{target}&a's bounty."
|
||||
claim-broadcast: "{prefix}&c{killer}&6 claimed &f{payout}&6 for killing &c{target}&6."
|
||||
expired: "{prefix}&7The bounty on &f{target}&7 expired."
|
||||
removed: "{prefix}&aRemoved the bounty on &f{target}&a."
|
||||
set: "{prefix}&aSet &f{target}&a's bounty to &f{amount}&a."
|
||||
clearall-warning: "{prefix}&cUse &f/bountyadmin clearall confirm &cto remove all active bounties."
|
||||
clearall-done: "{prefix}&aRemoved &f{count}&a active bounties."
|
||||
list-line: "&8- &c{target} &7» &f{amount} &8({contributors} contributors)"
|
||||
top-line: "&6#{rank} &c{target} &7» &f{amount}"
|
||||
view:
|
||||
- "{prefix}&6&lBounty: &c{target}"
|
||||
- "&7Amount: &f{amount}"
|
||||
- "&7Contributors: &f{contributors}"
|
||||
- "&7Expires: &f{expires}"
|
||||
- "&7Reason: &f{reason}"
|
||||
claiminfo:
|
||||
- "{prefix}&6&lClaim Rules for &c{target}"
|
||||
- "&8- &7PvP kill required: &f{pvp}"
|
||||
- "&8- &7Same-IP claims blocked: &f{same_ip}"
|
||||
- "&8- &7Allowed worlds: &f{worlds}"
|
||||
- "&8- &7Combat requirement: &f{combat}"
|
||||
|
||||
claim-denied:
|
||||
no-bounty: "No active bounty exists."
|
||||
no-permission: "You do not have permission to claim bounties."
|
||||
self: "Self-claims are blocked."
|
||||
same-ip: "Same-IP bounty claims are blocked."
|
||||
shared-known-ip: "Shared known-IP bounty claims are blocked."
|
||||
world: "Claims are disabled in this world."
|
||||
gamemode: "Your game mode cannot claim bounties."
|
||||
combat: "Combat requirements were not met."
|
||||
cooldown: "A claim cooldown is active."
|
||||
pair-limit: "This killer-target pair has too many recent claims."
|
||||
target-offline: "The target must be online at death."
|
||||
|
||||
gui:
|
||||
unavailable: "{prefix}&cThe bounty GUI is disabled."
|
||||
prompt-target: "{prefix}&eType the target player's name in chat, or type &fcancel&e."
|
||||
prompt-amount: "{prefix}&eType the bounty amount for &f{target}&e, or type &fcancel&e."
|
||||
prompt-reason: "{prefix}&eType a short reason for &f{target}&e, use &f-&e for none, or type &fcancel&e."
|
||||
input-cancelled: "{prefix}&7Bounty input cancelled."
|
||||
input-expired: "{prefix}&cYour bounty input expired."
|
||||
confirm-opened: "{prefix}&7Review and confirm the bounty."
|
||||
admin-opened: "{prefix}&7Opened the admin bounty view."
|
||||
|
||||
admin:
|
||||
history-empty: "{prefix}&7No bounty history found for &f{target}&7."
|
||||
suspicious-empty: "{prefix}&7No suspicious bounty activity has been logged."
|
||||
suspicious-line: "&8- &c{time} &7{type}: &f{details}"
|
||||
history-line: "&8- &e{time} &7{type}: &f{amount} &8({note})"
|
||||
refunded: "{prefix}&aRefunded &f{amount}&a to &f{player}&a."
|
||||
action-logged: "{prefix}&7Admin action logged."
|
||||
|
||||
webhook:
|
||||
placement-title: "New bounty placed"
|
||||
claim-title: "Bounty claimed"
|
||||
admin-title: "Bounty admin action"
|
||||
@@ -0,0 +1,67 @@
|
||||
name: DirtBounties
|
||||
version: 1.0.0
|
||||
main: com.dirtbagmc.dirtbounties.DirtBountiesPlugin
|
||||
api-version: '1.21'
|
||||
author: DirtbagMC
|
||||
website: https://dirtbagmc.com
|
||||
description: Premium bounty system for Paper SMP and anarchy servers.
|
||||
softdepend:
|
||||
- Vault
|
||||
- CMI
|
||||
- PlaceholderAPI
|
||||
|
||||
commands:
|
||||
bounty:
|
||||
description: Open and manage player bounties.
|
||||
usage: /bounty help
|
||||
aliases:
|
||||
- bounties
|
||||
- dbounty
|
||||
bountyadmin:
|
||||
description: Admin controls for DirtBounties.
|
||||
usage: /bountyadmin help
|
||||
aliases:
|
||||
- dbountyadmin
|
||||
- ba
|
||||
|
||||
permissions:
|
||||
dirtbounties.use:
|
||||
description: Allows opening the bounty GUI and using basic commands.
|
||||
default: true
|
||||
dirtbounties.place:
|
||||
description: Allows placing new bounties.
|
||||
default: true
|
||||
dirtbounties.add:
|
||||
description: Allows increasing existing bounties.
|
||||
default: true
|
||||
dirtbounties.view:
|
||||
description: Allows viewing bounty details.
|
||||
default: true
|
||||
dirtbounties.top:
|
||||
description: Allows viewing top bounties.
|
||||
default: true
|
||||
dirtbounties.claim:
|
||||
description: Allows claiming bounties by killing targets.
|
||||
default: true
|
||||
dirtbounties.anonymous:
|
||||
description: Allows placing anonymous bounties when enabled.
|
||||
default: op
|
||||
dirtbounties.bypass.cooldowns:
|
||||
description: Bypasses placement and claim cooldown checks.
|
||||
default: op
|
||||
dirtbounties.bypass.claimrules:
|
||||
description: Bypasses configured claim restrictions.
|
||||
default: op
|
||||
dirtbounties.admin:
|
||||
description: Full DirtBounties administration access.
|
||||
default: op
|
||||
children:
|
||||
dirtbounties.use: true
|
||||
dirtbounties.place: true
|
||||
dirtbounties.add: true
|
||||
dirtbounties.view: true
|
||||
dirtbounties.top: true
|
||||
dirtbounties.claim: true
|
||||
dirtbounties.anonymous: true
|
||||
dirtbounties.bypass.cooldowns: true
|
||||
dirtbounties.bypass.claimrules: true
|
||||
@@ -0,0 +1,3 @@
|
||||
artifactId=DirtBounties
|
||||
groupId=com.dirtbagmc
|
||||
version=1.0.0
|
||||
@@ -0,0 +1,36 @@
|
||||
com/dirtbagmc/dirtbounties/util/NumberUtil.class
|
||||
com/dirtbagmc/dirtbounties/service/BountyService.class
|
||||
com/dirtbagmc/dirtbounties/listener/BountyListener.class
|
||||
com/dirtbagmc/dirtbounties/service/PlayerCacheService.class
|
||||
com/dirtbagmc/dirtbounties/command/BountyAdminCommand.class
|
||||
com/dirtbagmc/dirtbounties/command/BountyCommand.class
|
||||
com/dirtbagmc/dirtbounties/service/HistoryService.class
|
||||
com/dirtbagmc/dirtbounties/service/CombatTracker.class
|
||||
com/dirtbagmc/dirtbounties/service/BountyService$Fee.class
|
||||
com/dirtbagmc/dirtbounties/webhook/WebhookService.class
|
||||
com/dirtbagmc/dirtbounties/gui/GuiManager$1.class
|
||||
com/dirtbagmc/dirtbounties/service/AntiAbuseService.class
|
||||
com/dirtbagmc/dirtbounties/model/BountyContribution.class
|
||||
com/dirtbagmc/dirtbounties/gui/GuiHolder.class
|
||||
com/dirtbagmc/dirtbounties/gui/GuiType.class
|
||||
com/dirtbagmc/dirtbounties/util/TimeUtil.class
|
||||
com/dirtbagmc/dirtbounties/DirtBountiesPlugin.class
|
||||
com/dirtbagmc/dirtbounties/hook/DirtBountiesExpansion.class
|
||||
com/dirtbagmc/dirtbounties/service/CombatTracker$CombatRecord.class
|
||||
com/dirtbagmc/dirtbounties/economy/EconomyService$EconomyResult.class
|
||||
com/dirtbagmc/dirtbounties/storage/StorageManager.class
|
||||
com/dirtbagmc/dirtbounties/util/ColorUtil.class
|
||||
com/dirtbagmc/dirtbounties/service/MessageService.class
|
||||
com/dirtbagmc/dirtbounties/model/Bounty.class
|
||||
com/dirtbagmc/dirtbounties/service/PlayerCacheService$CacheEntry.class
|
||||
com/dirtbagmc/dirtbounties/model/ClaimValidation.class
|
||||
com/dirtbagmc/dirtbounties/storage/StorageManager$PlayerCacheData.class
|
||||
com/dirtbagmc/dirtbounties/util/ItemBuilder.class
|
||||
com/dirtbagmc/dirtbounties/model/BountyHistoryRecord.class
|
||||
com/dirtbagmc/dirtbounties/command/BountyCommand$ReasonInput.class
|
||||
com/dirtbagmc/dirtbounties/gui/PendingBountyInput.class
|
||||
com/dirtbagmc/dirtbounties/gui/PendingBountyInput$Stage.class
|
||||
com/dirtbagmc/dirtbounties/config/ConfigService.class
|
||||
com/dirtbagmc/dirtbounties/economy/EconomyService.class
|
||||
com/dirtbagmc/dirtbounties/model/SuspiciousActivity.class
|
||||
com/dirtbagmc/dirtbounties/gui/GuiManager.class
|
||||
@@ -0,0 +1,28 @@
|
||||
/home/bitnix/Desktop/DirtBounties/src/main/java/com/dirtbagmc/dirtbounties/DirtBountiesPlugin.java
|
||||
/home/bitnix/Desktop/DirtBounties/src/main/java/com/dirtbagmc/dirtbounties/command/BountyAdminCommand.java
|
||||
/home/bitnix/Desktop/DirtBounties/src/main/java/com/dirtbagmc/dirtbounties/command/BountyCommand.java
|
||||
/home/bitnix/Desktop/DirtBounties/src/main/java/com/dirtbagmc/dirtbounties/config/ConfigService.java
|
||||
/home/bitnix/Desktop/DirtBounties/src/main/java/com/dirtbagmc/dirtbounties/economy/EconomyService.java
|
||||
/home/bitnix/Desktop/DirtBounties/src/main/java/com/dirtbagmc/dirtbounties/gui/GuiHolder.java
|
||||
/home/bitnix/Desktop/DirtBounties/src/main/java/com/dirtbagmc/dirtbounties/gui/GuiManager.java
|
||||
/home/bitnix/Desktop/DirtBounties/src/main/java/com/dirtbagmc/dirtbounties/gui/GuiType.java
|
||||
/home/bitnix/Desktop/DirtBounties/src/main/java/com/dirtbagmc/dirtbounties/gui/PendingBountyInput.java
|
||||
/home/bitnix/Desktop/DirtBounties/src/main/java/com/dirtbagmc/dirtbounties/hook/DirtBountiesExpansion.java
|
||||
/home/bitnix/Desktop/DirtBounties/src/main/java/com/dirtbagmc/dirtbounties/listener/BountyListener.java
|
||||
/home/bitnix/Desktop/DirtBounties/src/main/java/com/dirtbagmc/dirtbounties/model/Bounty.java
|
||||
/home/bitnix/Desktop/DirtBounties/src/main/java/com/dirtbagmc/dirtbounties/model/BountyContribution.java
|
||||
/home/bitnix/Desktop/DirtBounties/src/main/java/com/dirtbagmc/dirtbounties/model/BountyHistoryRecord.java
|
||||
/home/bitnix/Desktop/DirtBounties/src/main/java/com/dirtbagmc/dirtbounties/model/ClaimValidation.java
|
||||
/home/bitnix/Desktop/DirtBounties/src/main/java/com/dirtbagmc/dirtbounties/model/SuspiciousActivity.java
|
||||
/home/bitnix/Desktop/DirtBounties/src/main/java/com/dirtbagmc/dirtbounties/service/AntiAbuseService.java
|
||||
/home/bitnix/Desktop/DirtBounties/src/main/java/com/dirtbagmc/dirtbounties/service/BountyService.java
|
||||
/home/bitnix/Desktop/DirtBounties/src/main/java/com/dirtbagmc/dirtbounties/service/CombatTracker.java
|
||||
/home/bitnix/Desktop/DirtBounties/src/main/java/com/dirtbagmc/dirtbounties/service/HistoryService.java
|
||||
/home/bitnix/Desktop/DirtBounties/src/main/java/com/dirtbagmc/dirtbounties/service/MessageService.java
|
||||
/home/bitnix/Desktop/DirtBounties/src/main/java/com/dirtbagmc/dirtbounties/service/PlayerCacheService.java
|
||||
/home/bitnix/Desktop/DirtBounties/src/main/java/com/dirtbagmc/dirtbounties/storage/StorageManager.java
|
||||
/home/bitnix/Desktop/DirtBounties/src/main/java/com/dirtbagmc/dirtbounties/util/ColorUtil.java
|
||||
/home/bitnix/Desktop/DirtBounties/src/main/java/com/dirtbagmc/dirtbounties/util/ItemBuilder.java
|
||||
/home/bitnix/Desktop/DirtBounties/src/main/java/com/dirtbagmc/dirtbounties/util/NumberUtil.java
|
||||
/home/bitnix/Desktop/DirtBounties/src/main/java/com/dirtbagmc/dirtbounties/util/TimeUtil.java
|
||||
/home/bitnix/Desktop/DirtBounties/src/main/java/com/dirtbagmc/dirtbounties/webhook/WebhookService.java
|
||||
Reference in New Issue
Block a user