first commit

This commit is contained in:
2026-06-20 12:00:25 -04:00
commit af59d5beb5
14 changed files with 1039 additions and 0 deletions
@@ -0,0 +1,630 @@
package com.bitnix.dirttaxes;
import net.milkbowl.vault.economy.Economy;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.PluginCommand;
import org.bukkit.command.TabExecutor;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.plugin.RegisteredServiceProvider;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitTask;
import java.io.File;
import java.io.IOException;
import java.text.DecimalFormat;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Random;
import java.util.UUID;
public class DirtTaxesPlugin extends JavaPlugin implements TabExecutor {
private Economy economy;
private BukkitTask taxTask;
private File pendingFile;
private FileConfiguration pendingConfig;
private final Random random = new Random();
private final DecimalFormat moneyFormat = new DecimalFormat("0.00");
@Override
public void onEnable() {
saveDefaultConfig();
createDataFiles();
if (!setupEconomy()) {
getLogger().severe("Vault economy provider not found. Disabling DirtTaxes.");
getServer().getPluginManager().disablePlugin(this);
return;
}
PluginCommand command = getCommand("dirttaxes");
if (command != null) {
command.setExecutor(this);
command.setTabCompleter(this);
}
scheduleTaxTask();
registerJoinListener();
if (getConfig().getBoolean("tax.startup-tax-check", false)) {
long delayTicks = Math.max(20L, getConfig().getLong("tax.startup-tax-delay-seconds", 30L) * 20L);
Bukkit.getScheduler().runTaskLater(this, this::runOfflineTaxCycle, delayTicks);
}
getLogger().info("DirtTaxes enabled.");
}
@Override
public void onDisable() {
if (taxTask != null) {
taxTask.cancel();
taxTask = null;
}
Bukkit.getScheduler().cancelTasks(this);
savePendingMessages();
}
private void createDataFiles() {
if (!getDataFolder().exists()) {
getDataFolder().mkdirs();
}
pendingFile = new File(getDataFolder(), "pending_messages.yml");
if (!pendingFile.exists()) {
try {
pendingFile.createNewFile();
} catch (IOException e) {
getLogger().severe("Could not create pending_messages.yml");
e.printStackTrace();
}
}
pendingConfig = YamlConfiguration.loadConfiguration(pendingFile);
}
private boolean setupEconomy() {
RegisteredServiceProvider<Economy> rsp = getServer().getServicesManager().getRegistration(Economy.class);
if (rsp == null) {
return false;
}
economy = rsp.getProvider();
return economy != null;
}
private void scheduleTaxTask() {
if (taxTask != null) {
taxTask.cancel();
taxTask = null;
}
long intervalMinutes = Math.max(1L, getConfig().getLong("tax.interval-minutes", 1440L));
long intervalTicks = intervalMinutes * 60L * 20L;
taxTask = Bukkit.getScheduler().runTaskTimer(this, this::runOfflineTaxCycle, intervalTicks, intervalTicks);
}
private void registerJoinListener() {
getServer().getPluginManager().registerEvents(new TaxJoinListener(this), this);
}
public void handleJoin(Player player) {
String path = "players." + player.getUniqueId();
if (!pendingConfig.contains(path)) {
return;
}
List<String> messages = pendingConfig.getStringList(path + ".messages");
for (String message : messages) {
player.sendMessage(color(message));
}
pendingConfig.set(path, null);
savePendingMessages();
}
private void runOfflineTaxCycle() {
if (!getConfig().getBoolean("tax.enabled", true)) {
return;
}
long requiredOfflineMinutes = Math.max(0L, getConfig().getLong("tax.offline-minutes-required", 1440L));
long requiredOfflineMillis = requiredOfflineMinutes * 60L * 1000L;
for (OfflinePlayer offlinePlayer : Bukkit.getOfflinePlayers()) {
if (offlinePlayer == null) {
continue;
}
if (offlinePlayer.isOnline()) {
continue;
}
if (offlinePlayer.getName() == null) {
continue;
}
long lastPlayed = offlinePlayer.getLastPlayed();
if (lastPlayed <= 0L) {
continue;
}
long offlineFor = Instant.now().toEpochMilli() - lastPlayed;
if (offlineFor < requiredOfflineMillis) {
continue;
}
taxSinglePlayer(offlinePlayer, true);
}
if (getConfig().getBoolean("storage.save-pending-messages", true)) {
savePendingMessages();
}
}
private boolean taxSinglePlayer(OfflinePlayer offlinePlayer, boolean automatic) {
if (offlinePlayer == null || offlinePlayer.getName() == null) {
return false;
}
if (getConfig().getBoolean("tax.permission-exempt", true) && offlinePlayer.isOnline()) {
Player player = offlinePlayer.getPlayer();
if (player != null && player.hasPermission("dirttaxes.exempt")) {
return false;
}
}
if (getConfig().getBoolean("tax.op-exempt", true) && offlinePlayer.isOp()) {
return false;
}
double balance = economy.getBalance(offlinePlayer);
double oldBalance = balance;
double newBalance = balance;
double amountTaken = 0.0;
double maxDebt = getConfig().getDouble("tax.max-debt", -1000000.0);
if (maxDebt > 0) {
maxDebt = 0;
}
if (balance > 0) {
double ratePercent = Math.max(0.0, getConfig().getDouble("tax.rate-percent", 5.0));
double minimumTax = Math.max(0.0, getConfig().getDouble("tax.minimum-tax", 1.0));
amountTaken = Math.max(minimumTax, balance * (ratePercent / 100.0));
if (amountTaken > balance) {
amountTaken = balance;
}
if (getConfig().getBoolean("tax.round-to-cents", true)) {
amountTaken = roundMoney(amountTaken);
}
if (amountTaken <= 0.0) {
return false;
}
economy.withdrawPlayer(offlinePlayer, amountTaken);
newBalance = economy.getBalance(offlinePlayer);
if (sameMoney(oldBalance, newBalance)) {
return false;
}
amountTaken = Math.abs(oldBalance - newBalance);
} else if (getConfig().getBoolean("debt.enabled", true)) {
double interestPercent = Math.max(0.0, getConfig().getDouble("debt.negative-interest-percent", 2.5));
double flatDebt = Math.max(0.0, getConfig().getDouble("debt.flat-debt-per-cycle", 500.0));
double debtIncrease = Math.abs(balance) * (interestPercent / 100.0) + flatDebt;
if (getConfig().getBoolean("tax.round-to-cents", true)) {
debtIncrease = roundMoney(debtIncrease);
}
if (debtIncrease <= 0.0) {
return false;
}
double targetBalance = balance - debtIncrease;
if (targetBalance < maxDebt) {
targetBalance = maxDebt;
}
amountTaken = Math.abs(targetBalance - balance);
if (amountTaken <= 0.0) {
return false;
}
boolean usedCommand = getConfig().getBoolean("debt.use-command-for-debt", true);
if (usedCommand) {
runDebtCommand(offlinePlayer, amountTaken, balance, targetBalance);
newBalance = economy.getBalance(offlinePlayer);
if (sameMoney(oldBalance, newBalance)) {
if (getConfig().getBoolean("debt.warn-if-command-fails", true)) {
String fail = prefixed("messages.debt-command-failed").replace("%player%", offlinePlayer.getName());
Bukkit.getConsoleSender().sendMessage(fail);
for (Player online : Bukkit.getOnlinePlayers()) {
if (online.hasPermission("dirttaxes.notify")) {
online.sendMessage(fail);
}
}
}
return false;
}
amountTaken = Math.abs(oldBalance - newBalance);
} else {
economy.withdrawPlayer(offlinePlayer, amountTaken);
newBalance = economy.getBalance(offlinePlayer);
if (sameMoney(oldBalance, newBalance)) {
return false;
}
amountTaken = Math.abs(oldBalance - newBalance);
}
} else {
return false;
}
if (getConfig().getBoolean("announcement.enabled", true)) {
announceTax(offlinePlayer, amountTaken, newBalance);
}
if (automatic && getConfig().getBoolean("announcement.store-offline-message", true)) {
storeJoinMessages(offlinePlayer.getUniqueId(), amountTaken, newBalance);
}
if (newBalance < 0 && getConfig().getBoolean("debt.announce-debt", true)) {
String debtMsg = getConfig().getString("debt.debt-message", "&4IRS &8- &c%player% is now drowning in debt. Balance: $%new_balance%");
debtMsg = debtMsg.replace("%player%", offlinePlayer.getName())
.replace("%new_balance%", formatMoney(newBalance));
broadcastToStaffAndConsole(color(debtMsg));
}
return true;
}
private void runDebtCommand(OfflinePlayer offlinePlayer, double amountTaken, double oldBalance, double targetBalance) {
String raw = getConfig().getString("debt.command", "cmi money set %player% %new_balance%");
if (raw == null || raw.isBlank()) {
return;
}
String command = raw
.replace("%player%", offlinePlayer.getName())
.replace("%amount%", formatMoney(amountTaken))
.replace("%old_balance%", formatMoney(oldBalance))
.replace("%new_balance%", formatMoney(targetBalance));
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), command);
}
private void announceTax(OfflinePlayer offlinePlayer, double amountTaken, double newBalance) {
String message = getConfig().getString("announcement.staff-message",
"&6IRS &8- &eTook &c$%amount% &efrom &b%player%&e. New balance: &a$%new_balance%");
message = message.replace("%amount%", formatMoney(amountTaken))
.replace("%player%", offlinePlayer.getName())
.replace("%new_balance%", formatMoney(newBalance));
broadcastToStaffAndConsole(color(message));
if (getConfig().getBoolean("fun.enabled", true)
&& getConfig().getBoolean("fun.include-funny-line-in-staff-alerts", true)
&& getConfig().getBoolean("fun.random-funny-lines", true)) {
String funny = getRandomFunnyLine();
if (funny != null && !funny.isBlank()) {
broadcastToStaffAndConsole(color(funny));
}
}
}
private void broadcastToStaffAndConsole(String message) {
if (getConfig().getBoolean("announcement.notify-online-staff", true)) {
for (Player player : Bukkit.getOnlinePlayers()) {
if (player.hasPermission("dirttaxes.notify")) {
player.sendMessage(message);
}
}
}
if (getConfig().getBoolean("announcement.log-to-console", true)) {
Bukkit.getConsoleSender().sendMessage(message);
}
}
private void storeJoinMessages(UUID uuid, double amountTaken, double newBalance) {
String path = "players." + uuid + ".messages";
List<String> messages = pendingConfig.getStringList(path);
String joinMessage = getConfig().getString("announcement.join-message",
"&6IRS &8- &eTook &c$%amount% &efrom your account while you were away. New balance: &a$%new_balance%");
joinMessage = joinMessage.replace("%amount%", formatMoney(amountTaken))
.replace("%new_balance%", formatMoney(newBalance));
messages.add(joinMessage);
if (getConfig().getBoolean("fun.enabled", true)
&& getConfig().getBoolean("fun.send-funny-line-to-player", true)
&& getConfig().getBoolean("fun.random-funny-lines", true)) {
String funny = getRandomFunnyLine();
if (funny != null && !funny.isBlank()) {
messages.add(funny);
}
}
pendingConfig.set(path, messages);
}
private String getRandomFunnyLine() {
List<String> lines = getConfig().getStringList("fun.funny-lines");
if (lines.isEmpty()) {
return null;
}
return lines.get(random.nextInt(lines.size()));
}
private void savePendingMessages() {
if (pendingConfig == null || pendingFile == null) {
return;
}
try {
pendingConfig.save(pendingFile);
} catch (IOException e) {
getLogger().severe("Failed to save pending_messages.yml");
e.printStackTrace();
}
}
private double roundMoney(double amount) {
return Math.round(amount * 100.0D) / 100.0D;
}
private boolean sameMoney(double a, double b) {
return Math.abs(a - b) < 0.009D;
}
private String formatMoney(double amount) {
return moneyFormat.format(amount);
}
private String color(String text) {
return ChatColor.translateAlternateColorCodes('&', text);
}
private String msg(String path) {
return color(getConfig().getString(path, path));
}
private String prefixed(String path) {
return msg("messages.prefix") + msg(path);
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (args.length == 0) {
if (!sender.hasPermission("dirttaxes.use")) {
sender.sendMessage(prefixed("messages.no-permission"));
return true;
}
String info = msg("messages.info")
.replace("%rate%", String.valueOf(getConfig().getDouble("tax.rate-percent", 5.0)))
.replace("%interval%", String.valueOf(getConfig().getLong("tax.interval-minutes", 1440L)))
.replace("%maxdebt%", formatMoney(getConfig().getDouble("tax.max-debt", -1000000.0)));
sender.sendMessage(msg("messages.prefix") + info);
sender.sendMessage(msg("messages.prefix") + msg("messages.usage"));
return true;
}
String sub = args[0].toLowerCase(Locale.ROOT);
switch (sub) {
case "reload" -> {
if (!sender.hasPermission("dirttaxes.reload")) {
sender.sendMessage(prefixed("messages.no-permission"));
return true;
}
reloadConfig();
scheduleTaxTask();
sender.sendMessage(prefixed("messages.reloaded"));
return true;
}
case "info" -> {
if (!sender.hasPermission("dirttaxes.use")) {
sender.sendMessage(prefixed("messages.no-permission"));
return true;
}
String info = msg("messages.info")
.replace("%rate%", String.valueOf(getConfig().getDouble("tax.rate-percent", 5.0)))
.replace("%interval%", String.valueOf(getConfig().getLong("tax.interval-minutes", 1440L)))
.replace("%maxdebt%", formatMoney(getConfig().getDouble("tax.max-debt", -1000000.0)));
sender.sendMessage(msg("messages.prefix") + info);
return true;
}
case "setrate" -> {
if (!sender.hasPermission("dirttaxes.setrate")) {
sender.sendMessage(prefixed("messages.no-permission"));
return true;
}
if (args.length < 2) {
sender.sendMessage(prefixed("messages.usage"));
return true;
}
Double value = parseDouble(args[1]);
if (value == null) {
sender.sendMessage(prefixed("messages.invalid-number"));
return true;
}
if (value < 0) {
sender.sendMessage(prefixed("messages.rate-must-be-nonnegative"));
return true;
}
getConfig().set("tax.rate-percent", value);
saveConfig();
sender.sendMessage(prefixed("messages.set-rate-success").replace("%rate%", String.valueOf(value)));
return true;
}
case "setmaxdebt" -> {
if (!sender.hasPermission("dirttaxes.setmaxdebt")) {
sender.sendMessage(prefixed("messages.no-permission"));
return true;
}
if (args.length < 2) {
sender.sendMessage(prefixed("messages.usage"));
return true;
}
Double value = parseDouble(args[1]);
if (value == null) {
sender.sendMessage(prefixed("messages.invalid-number"));
return true;
}
if (value > 0) {
sender.sendMessage(prefixed("messages.max-debt-must-be-negative"));
return true;
}
getConfig().set("tax.max-debt", value);
saveConfig();
sender.sendMessage(prefixed("messages.set-max-debt-success").replace("%maxdebt%", formatMoney(value)));
return true;
}
case "setinterval" -> {
if (!sender.hasPermission("dirttaxes.setinterval")) {
sender.sendMessage(prefixed("messages.no-permission"));
return true;
}
if (args.length < 2) {
sender.sendMessage(prefixed("messages.usage"));
return true;
}
Long value = parseLong(args[1]);
if (value == null) {
sender.sendMessage(prefixed("messages.invalid-number"));
return true;
}
if (value <= 0) {
sender.sendMessage(prefixed("messages.interval-must-be-positive"));
return true;
}
getConfig().set("tax.interval-minutes", value);
saveConfig();
scheduleTaxTask();
sender.sendMessage(prefixed("messages.set-interval-success").replace("%interval%", String.valueOf(value)));
return true;
}
case "forcetax" -> {
if (!sender.hasPermission("dirttaxes.forcetax")) {
sender.sendMessage(prefixed("messages.no-permission"));
return true;
}
if (args.length >= 2) {
OfflinePlayer target = Bukkit.getOfflinePlayer(args[1]);
if (target == null || target.getName() == null) {
sender.sendMessage(prefixed("messages.player-not-found"));
return true;
}
taxSinglePlayer(target, true);
sender.sendMessage(prefixed("messages.force-tax-player-success").replace("%player%", target.getName()));
return true;
}
runOfflineTaxCycle();
sender.sendMessage(prefixed("messages.force-tax-all-success"));
return true;
}
default -> {
sender.sendMessage(prefixed("messages.usage"));
return true;
}
}
}
private Double parseDouble(String input) {
try {
return Double.parseDouble(input);
} catch (NumberFormatException ignored) {
return null;
}
}
private Long parseLong(String input) {
try {
return Long.parseLong(input);
} catch (NumberFormatException ignored) {
return null;
}
}
@Override
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
if (args.length == 1) {
List<String> subs = new ArrayList<>();
subs.add("reload");
subs.add("info");
subs.add("setrate");
subs.add("setmaxdebt");
subs.add("setinterval");
subs.add("forcetax");
return partial(args[0], subs);
}
if (args.length == 2 && args[0].equalsIgnoreCase("forcetax")) {
List<String> names = new ArrayList<>();
for (OfflinePlayer player : Bukkit.getOfflinePlayers()) {
if (player.getName() != null) {
names.add(player.getName());
}
}
return partial(args[1], names);
}
return Collections.emptyList();
}
private List<String> partial(String input, List<String> options) {
String lower = input.toLowerCase(Locale.ROOT);
List<String> matches = new ArrayList<>();
for (String option : options) {
if (option.toLowerCase(Locale.ROOT).startsWith(lower)) {
matches.add(option);
}
}
return matches;
}
}
@@ -0,0 +1,19 @@
package com.bitnix.dirttaxes;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
public class TaxJoinListener implements Listener {
private final DirtTaxesPlugin plugin;
public TaxJoinListener(DirtTaxesPlugin plugin) {
this.plugin = plugin;
}
@EventHandler
public void onJoin(PlayerJoinEvent event) {
plugin.handleJoin(event.getPlayer());
}
}