Add Coal Cents economy

This commit is contained in:
CoalGov Deploy
2026-05-04 17:00:05 +00:00
commit c8cc200004
73 changed files with 6873 additions and 0 deletions

View File

@@ -0,0 +1,251 @@
package com.librewiki.coalgov;
import com.librewiki.coalgov.command.AdminCommand;
import com.librewiki.coalgov.command.ClaimCommand;
import com.librewiki.coalgov.command.CoalCommand;
import com.librewiki.coalgov.command.DiviningRodCommand;
import com.librewiki.coalgov.command.LandCommand;
import com.librewiki.coalgov.command.MarketCommand;
import com.librewiki.coalgov.command.NpcCommand;
import com.librewiki.coalgov.command.PermitCommand;
import com.librewiki.coalgov.command.PoliceCommand;
import com.librewiki.coalgov.listener.BlockBreakListener;
import com.librewiki.coalgov.listener.BlockPlaceListener;
import com.librewiki.coalgov.listener.ClaimWandListener;
import com.librewiki.coalgov.listener.DiviningRodListener;
import com.librewiki.coalgov.listener.MarketListener;
import com.librewiki.coalgov.listener.NpcListener;
import com.librewiki.coalgov.listener.PlayerJoinListener;
import com.librewiki.coalgov.listener.SuperFurnaceListener;
import com.librewiki.coalgov.service.BuildProtectionService;
import com.librewiki.coalgov.service.ClaimMarkerService;
import com.librewiki.coalgov.service.ClaimSelectionService;
import com.librewiki.coalgov.service.ClaimService;
import com.librewiki.coalgov.service.ClaimVisualService;
import com.librewiki.coalgov.service.DiviningRodService;
import com.librewiki.coalgov.service.EconomyService;
import com.librewiki.coalgov.service.GovernmentService;
import com.librewiki.coalgov.service.LandService;
import com.librewiki.coalgov.service.LandAppraisalService;
import com.librewiki.coalgov.service.MarketService;
import com.librewiki.coalgov.service.MiningService;
import com.librewiki.coalgov.service.NpcService;
import com.librewiki.coalgov.service.OpenRouterService;
import com.librewiki.coalgov.service.PermitService;
import com.librewiki.coalgov.service.SpawnGuideService;
import com.librewiki.coalgov.service.SuperFurnaceService;
import com.librewiki.coalgov.service.TaxService;
import com.librewiki.coalgov.storage.ClaimRepository;
import com.librewiki.coalgov.storage.Database;
import com.librewiki.coalgov.storage.EconomyRepository;
import com.librewiki.coalgov.storage.GovernmentRepository;
import com.librewiki.coalgov.storage.LandRepository;
import com.librewiki.coalgov.storage.MarketRepository;
import com.librewiki.coalgov.storage.NpcRepository;
import com.librewiki.coalgov.storage.PermitRepository;
import com.librewiki.coalgov.storage.SuperFurnaceRepository;
import com.librewiki.coalgov.util.CoalMoney;
import com.librewiki.coalgov.util.Messages;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import java.sql.SQLException;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
public final class CoalGovPlugin extends JavaPlugin {
private Database database;
private EconomyService economyService;
private GovernmentService governmentService;
private ClaimService claimService;
private ClaimMarkerService claimMarkerService;
private ClaimSelectionService claimSelectionService;
private ClaimVisualService claimVisualService;
private BuildProtectionService buildProtectionService;
private LandAppraisalService landAppraisalService;
private MarketService marketService;
private NpcService npcService;
private OpenRouterService openRouterService;
private LandService landService;
private PermitService permitService;
private MiningService miningService;
private DiviningRodService diviningRodService;
private SuperFurnaceService superFurnaceService;
private SpawnGuideService spawnGuideService;
private TaxService taxService;
private Messages messages;
private final Set<UUID> adminBypassPlayers = new HashSet<>();
@Override
public void onEnable() {
saveDefaultConfig();
messages = new Messages(getConfig());
database = new Database(getDataFolder());
try {
database.open();
} catch (SQLException exception) {
getLogger().severe("Failed to initialize SQLite database: " + exception.getMessage());
getServer().getPluginManager().disablePlugin(this);
return;
}
EconomyRepository economyRepository = new EconomyRepository(database);
LandRepository landRepository = new LandRepository(database);
ClaimRepository claimRepository = new ClaimRepository(database);
PermitRepository permitRepository = new PermitRepository(database);
MarketRepository marketRepository = new MarketRepository(database);
GovernmentRepository governmentRepository = new GovernmentRepository(database);
SuperFurnaceRepository superFurnaceRepository = new SuperFurnaceRepository(database);
NpcRepository npcRepository = new NpcRepository(database);
economyService = new EconomyService(economyRepository, CoalMoney.fromCoalConfig(getConfig().getDouble("economy.starting_balance", 0.0D)));
governmentService = new GovernmentService(governmentRepository, economyService, getConfig());
landService = new LandService(landRepository, getConfig());
claimService = new ClaimService(claimRepository, landService, economyService, getConfig());
claimMarkerService = new ClaimMarkerService(this, claimService);
claimSelectionService = new ClaimSelectionService(this);
claimVisualService = new ClaimVisualService(this);
buildProtectionService = new BuildProtectionService(this, claimService);
landAppraisalService = new LandAppraisalService(this, claimService);
marketService = new MarketService(this, marketRepository);
npcService = new NpcService(this, npcRepository);
openRouterService = new OpenRouterService(this);
permitService = new PermitService(permitRepository, landService, economyService, getConfig());
miningService = new MiningService(this, landService, claimService, permitService);
diviningRodService = new DiviningRodService(this);
superFurnaceService = new SuperFurnaceService(this, superFurnaceRepository);
spawnGuideService = new SpawnGuideService(this);
taxService = new TaxService();
Objects.requireNonNull(getCommand("coal")).setExecutor(new CoalCommand(this));
Objects.requireNonNull(getCommand("claim")).setExecutor(new ClaimCommand(this));
Objects.requireNonNull(getCommand("permit")).setExecutor(new PermitCommand(this));
Objects.requireNonNull(getCommand("land")).setExecutor(new LandCommand(this));
Objects.requireNonNull(getCommand("market")).setExecutor(new MarketCommand(this));
Objects.requireNonNull(getCommand("diviningrod")).setExecutor(new DiviningRodCommand(this));
Objects.requireNonNull(getCommand("police")).setExecutor(new PoliceCommand(this));
Objects.requireNonNull(getCommand("cgnpc")).setExecutor(new NpcCommand(this));
Objects.requireNonNull(getCommand("coalgov")).setExecutor(new AdminCommand(this));
getServer().getPluginManager().registerEvents(new BlockBreakListener(this), this);
getServer().getPluginManager().registerEvents(new BlockPlaceListener(this), this);
getServer().getPluginManager().registerEvents(new ClaimWandListener(this), this);
getServer().getPluginManager().registerEvents(new DiviningRodListener(this), this);
getServer().getPluginManager().registerEvents(new MarketListener(this), this);
getServer().getPluginManager().registerEvents(new NpcListener(this), this);
getServer().getPluginManager().registerEvents(new PlayerJoinListener(this), this);
getServer().getPluginManager().registerEvents(new SuperFurnaceListener(this), this);
superFurnaceService.registerRecipe();
getServer().getScheduler().runTaskLater(this, () -> claimMarkerService.placeAllLoadedMarkers(), 40L);
long npcTicks = Math.max(20L, getConfig().getLong("npc.worker.tick_interval_seconds", 20L) * 20L);
getServer().getScheduler().runTaskTimer(this, () -> npcService.tickWorkers(), 80L, npcTicks);
getLogger().info("CoalGov enabled.");
}
@Override
public void onDisable() {
if (database != null) {
database.close();
}
}
public void reloadCoalGovConfig() {
reloadConfig();
messages = new Messages(getConfig());
governmentService.setConfig(getConfig());
landService.setConfig(getConfig());
claimService.setConfig(getConfig());
permitService.setConfig(getConfig());
superFurnaceService.registerRecipe();
}
public boolean hasAdminBypass(Player player) {
return player.hasPermission("coalgov.admin") && adminBypassPlayers.contains(player.getUniqueId());
}
public void setAdminBypass(Player player, boolean enabled) {
if (enabled) {
adminBypassPlayers.add(player.getUniqueId());
} else {
adminBypassPlayers.remove(player.getUniqueId());
}
}
public EconomyService economyService() {
return economyService;
}
public GovernmentService governmentService() {
return governmentService;
}
public ClaimService claimService() {
return claimService;
}
public ClaimVisualService claimVisualService() {
return claimVisualService;
}
public ClaimMarkerService claimMarkerService() {
return claimMarkerService;
}
public ClaimSelectionService claimSelectionService() {
return claimSelectionService;
}
public BuildProtectionService buildProtectionService() {
return buildProtectionService;
}
public LandAppraisalService landAppraisalService() {
return landAppraisalService;
}
public MarketService marketService() {
return marketService;
}
public NpcService npcService() {
return npcService;
}
public OpenRouterService openRouterService() {
return openRouterService;
}
public LandService landService() {
return landService;
}
public PermitService permitService() {
return permitService;
}
public MiningService miningService() {
return miningService;
}
public DiviningRodService diviningRodService() {
return diviningRodService;
}
public SuperFurnaceService superFurnaceService() {
return superFurnaceService;
}
public SpawnGuideService spawnGuideService() {
return spawnGuideService;
}
public TaxService taxService() {
return taxService;
}
public Messages messages() {
return messages;
}
}

View File

@@ -0,0 +1,213 @@
package com.librewiki.coalgov.command;
import com.librewiki.coalgov.CoalGovPlugin;
import com.librewiki.coalgov.util.CoalMoney;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.sql.SQLException;
public final class AdminCommand implements CommandExecutor {
private final CoalGovPlugin plugin;
public AdminCommand(CoalGovPlugin plugin) {
this.plugin = plugin;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (args.length == 0 || !args[0].equalsIgnoreCase("admin")) {
usage(sender);
return true;
}
if (!sender.hasPermission("coalgov.admin")) {
sender.sendMessage(plugin.messages().text("&cYou lack permission."));
return true;
}
try {
if (args.length >= 2 && args[1].equalsIgnoreCase("reload")) {
plugin.reloadCoalGovConfig();
sender.sendMessage(plugin.messages().text("&aConfig reloaded."));
return true;
}
if (args.length >= 2 && args[1].equalsIgnoreCase("bypass")) {
bypass(sender, args);
return true;
}
if (args.length >= 2 && args[1].equalsIgnoreCase("spawn")) {
spawn(sender, args);
return true;
}
if (args.length >= 2 && args[1].equalsIgnoreCase("rod")) {
rod(sender, args);
return true;
}
if (args.length >= 2 && args[1].equalsIgnoreCase("treasury")) {
treasury(sender, args);
return true;
}
if (args.length >= 2 && args[1].equalsIgnoreCase("tax")) {
tax(sender, args);
return true;
}
if (args.length < 3) {
usage(sender);
return true;
}
OfflinePlayer target = Bukkit.getOfflinePlayer(args[2]);
plugin.economyService().ensurePlayer(target);
switch (args[1].toLowerCase()) {
case "balance" -> sender.sendMessage(plugin.messages().text("&e" + args[2] + ": &f"
+ CoalMoney.format(plugin.economyService().balance(target.getUniqueId()))));
case "grant" -> grant(sender, target, args);
case "take" -> take(sender, target, args);
default -> usage(sender);
}
} catch (SQLException exception) {
plugin.getLogger().warning("Admin command failed: " + exception.getMessage());
sender.sendMessage(plugin.messages().text("&cCoalGov database unavailable."));
}
return true;
}
private void usage(CommandSender sender) {
sender.sendMessage(plugin.messages().text("&eUsage: /coalgov admin <balance|grant|take|reload|spawn|bypass|rod|treasury|tax>"));
}
private void bypass(CommandSender sender, String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage("Players only.");
return;
}
if (args.length == 2 || (args.length == 3 && args[2].equalsIgnoreCase("status"))) {
String status = plugin.hasAdminBypass(player) ? "&aON" : "&cOFF";
player.sendMessage(plugin.messages().text("&eAdmin bypass is " + status + "&e."));
return;
}
if (args.length != 3) {
player.sendMessage(plugin.messages().text("&eUsage: /coalgov admin bypass <on|off|status>"));
return;
}
if (args[2].equalsIgnoreCase("on")) {
plugin.setAdminBypass(player, true);
player.sendMessage(plugin.messages().text("&aAdmin bypass enabled. Build, mining, and marker restrictions are bypassed."));
return;
}
if (args[2].equalsIgnoreCase("off")) {
plugin.setAdminBypass(player, false);
player.sendMessage(plugin.messages().text("&aAdmin bypass disabled. You now play under normal CoalGov rules."));
return;
}
player.sendMessage(plugin.messages().text("&eUsage: /coalgov admin bypass <on|off|status>"));
}
private void spawn(CommandSender sender, String[] args) throws SQLException {
if (!(sender instanceof Player player)) {
sender.sendMessage("Players only.");
return;
}
if (args.length != 3 || !args[2].equalsIgnoreCase("setup")) {
player.sendMessage(plugin.messages().text("&eUsage: /coalgov admin spawn setup"));
return;
}
plugin.spawnGuideService().setup(player);
player.sendMessage(plugin.messages().text("&aSpawn set, protected, and guide signs placed."));
}
private void rod(CommandSender sender, String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage("Players only.");
return;
}
if (args.length != 2) {
player.sendMessage(plugin.messages().text("&eUsage: /coalgov admin rod"));
return;
}
player.getInventory().addItem(plugin.diviningRodService().rod());
player.sendMessage(plugin.messages().text("&aDivining rod granted."));
}
private void treasury(CommandSender sender, String[] args) throws SQLException {
if (args.length == 3 && args[2].equalsIgnoreCase("balance")) {
sender.sendMessage(plugin.messages().text("&eTreasury: &f" + CoalMoney.format(plugin.governmentService().treasuryBalance())));
return;
}
if (args.length == 5 && args[2].equalsIgnoreCase("grant")) {
OfflinePlayer target = Bukkit.getOfflinePlayer(args[3]);
plugin.economyService().ensurePlayer(target);
long amount = CoalMoney.parsePositive(args[4]);
if (amount <= 0L) {
sender.sendMessage(plugin.messages().text("&cAmount must be greater than zero."));
return;
}
if (!plugin.governmentService().grantFromTreasury(target.getUniqueId(), amount)) {
sender.sendMessage(plugin.messages().text("&cTreasury grant failed. Check treasury balance."));
return;
}
sender.sendMessage(plugin.messages().text("&aGranted &f" + CoalMoney.format(amount) + " &afrom the treasury."));
return;
}
sender.sendMessage(plugin.messages().text("&eUsage: /coalgov admin treasury <balance|grant <player> <amount>>"));
}
private void tax(CommandSender sender, String[] args) throws SQLException {
if (args.length != 5 || !args[2].equalsIgnoreCase("exempt")) {
sender.sendMessage(plugin.messages().text("&eUsage: /coalgov admin tax exempt <player> <on|off|status>"));
return;
}
OfflinePlayer target = Bukkit.getOfflinePlayer(args[3]);
plugin.economyService().ensurePlayer(target);
if (args[4].equalsIgnoreCase("status")) {
boolean exempt = plugin.governmentService().taxExempt(target.getUniqueId());
sender.sendMessage(plugin.messages().text("&eTax exemption for &f" + args[3] + "&e: " + (exempt ? "&aON" : "&cOFF")));
return;
}
if (args[4].equalsIgnoreCase("on")) {
plugin.governmentService().setTaxExempt(target.getUniqueId(), true);
sender.sendMessage(plugin.messages().text("&aTax exemption enabled for &f" + args[3] + "&a."));
return;
}
if (args[4].equalsIgnoreCase("off")) {
plugin.governmentService().setTaxExempt(target.getUniqueId(), false);
sender.sendMessage(plugin.messages().text("&aTax exemption disabled for &f" + args[3] + "&a."));
return;
}
sender.sendMessage(plugin.messages().text("&eUsage: /coalgov admin tax exempt <player> <on|off|status>"));
}
private void grant(CommandSender sender, OfflinePlayer target, String[] args) throws SQLException {
if (args.length != 4) {
sender.sendMessage(plugin.messages().text("&eUsage: /coalgov admin grant <player> <amount>"));
return;
}
long amount = CoalMoney.parsePositive(args[3]);
if (amount <= 0) {
sender.sendMessage(plugin.messages().text("&cAmount must be greater than zero."));
return;
}
plugin.economyService().grant(target.getUniqueId(), amount);
sender.sendMessage(plugin.messages().text("&aGranted &f" + CoalMoney.format(amount) + "&a."));
}
private void take(CommandSender sender, OfflinePlayer target, String[] args) throws SQLException {
if (args.length != 4) {
sender.sendMessage(plugin.messages().text("&eUsage: /coalgov admin take <player> <amount>"));
return;
}
long amount = CoalMoney.parsePositive(args[3]);
if (amount <= 0) {
sender.sendMessage(plugin.messages().text("&cAmount must be greater than zero."));
return;
}
if (plugin.economyService().take(target.getUniqueId(), amount)) {
sender.sendMessage(plugin.messages().text("&aTook &f" + CoalMoney.format(amount) + "&a."));
} else {
sender.sendMessage(plugin.messages().text("&cBalance too low."));
}
}
}

View File

@@ -0,0 +1,298 @@
package com.librewiki.coalgov.command;
import com.librewiki.coalgov.CoalGovPlugin;
import com.librewiki.coalgov.model.Claim;
import com.librewiki.coalgov.model.ClaimPoint;
import com.librewiki.coalgov.model.LandRegion;
import com.librewiki.coalgov.service.ClaimService;
import com.librewiki.coalgov.service.LandAppraisalService;
import com.librewiki.coalgov.util.CoalMoney;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.sql.SQLException;
import java.util.List;
import java.util.Optional;
public final class ClaimCommand implements CommandExecutor {
private final CoalGovPlugin plugin;
public ClaimCommand(CoalGovPlugin plugin) {
this.plugin = plugin;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage("Players only.");
return true;
}
if (args.length == 0) {
player.sendMessage(plugin.messages().text("&eUsage: /claim <wand|buy|info|list|show|appraise|sell|transfer|abandon>"));
return true;
}
try {
plugin.economyService().ensurePlayer(player);
switch (args[0].toLowerCase()) {
case "wand" -> wand(player);
case "buy" -> buy(player, args);
case "info" -> info(player);
case "list" -> list(player);
case "show" -> show(player, args);
case "appraise" -> appraise(player, args);
case "sell" -> sell(player, args);
case "transfer" -> transfer(player, args);
case "abandon" -> abandon(player, args);
default -> player.sendMessage(plugin.messages().text("&eUsage: /claim <wand|buy|info|list|show|appraise|sell|transfer|abandon>"));
}
} catch (SQLException exception) {
plugin.getLogger().warning("Claim command failed: " + exception.getMessage());
player.sendMessage(plugin.messages().text("&cClaim registry unavailable."));
}
return true;
}
private void buy(Player player, String[] args) throws SQLException {
if (args.length != 2 && args.length != 3) {
player.sendMessage(plugin.messages().text("&eUsage: /claim buy <homestead|industrial> [radius]"));
return;
}
ClaimService.BuyResult result;
if (args.length == 3) {
int radius = parseInt(args[2]);
result = plugin.claimService().buy(player.getUniqueId(), player.getLocation(), args[1], radius);
} else {
List<ClaimPoint> points = plugin.claimSelectionService().claimPoints(player);
result = plugin.claimService().buyPolygon(player.getUniqueId(), player.getWorld().getName(), args[1], points);
}
if (!result.success()) {
player.sendMessage(plugin.messages().text("&c" + result.message()));
return;
}
player.sendMessage(plugin.messages().text("&aClaim #" + result.id() + " created for &f" + CoalMoney.format(result.cost()) + "&a."));
plugin.claimSelectionService().consume(player);
plugin.claimService().findOwnedById(player.getUniqueId(), result.id()).ifPresent(claim -> {
plugin.claimMarkerService().placeMarkers(claim);
plugin.claimVisualService().show(player, claim);
});
}
private void wand(Player player) {
player.getInventory().addItem(plugin.claimSelectionService().wand());
player.sendMessage(plugin.messages().text("&aClaim wand issued. Right-click blocks to draw a polygon with marker previews; sneak right-click to clear."));
}
private void info(Player player) throws SQLException {
Optional<Claim> claim = plugin.claimService().claimAt(player.getLocation());
Optional<LandRegion> region = plugin.landService().regionAt(player.getLocation());
player.sendMessage(plugin.messages().text("&eLand: &f" + region.map(r -> r.name() + " " + r.landClass()).orElse(plugin.landService().classAt(player.getLocation()).name())));
if (claim.isPresent()) {
Claim c = claim.get();
player.sendMessage(plugin.messages().text("&eClaim: &f#" + c.id() + " " + c.claimType() + " owner " + c.ownerUuid()));
if (c.isPolygon()) {
player.sendMessage(plugin.messages().text("&eShape: &fpolygon with " + c.vertices().size() + " points"));
}
plugin.claimVisualService().show(player, c);
} else {
player.sendMessage(plugin.messages().text("&eClaim: &fnone"));
}
}
private void list(Player player) throws SQLException {
List<Claim> claims = plugin.claimService().list(player.getUniqueId());
if (claims.isEmpty()) {
player.sendMessage(plugin.messages().text("&eYou have no claims."));
return;
}
player.sendMessage(plugin.messages().text("&eYour claims:"));
for (Claim claim : claims) {
String shape = claim.isPolygon() ? " polygon" : " rectangle";
player.sendMessage(plugin.messages().raw("&7#" + claim.id() + " &f" + claim.claimType() + shape + " purchase " + CoalMoney.format(plugin.claimService().claimPurchaseCost(claim)) + " " + claim.world()
+ " [" + claim.x1() + "," + claim.z1() + "] to [" + claim.x2() + "," + claim.z2() + "]"));
}
player.sendMessage(plugin.messages().text("&7Use &f/claim appraise [id] &7for sale value, or &f/claim show <id>&7."));
}
private void show(Player player, String[] args) throws SQLException {
if (args.length != 2) {
player.sendMessage(plugin.messages().text("&eUsage: /claim show <id>"));
return;
}
long id = parseLong(args[1]);
if (id <= 0) {
player.sendMessage(plugin.messages().text("&cInvalid claim id."));
return;
}
Optional<Claim> claim = plugin.claimService().findOwnedById(player.getUniqueId(), id);
if (claim.isEmpty()) {
player.sendMessage(plugin.messages().text("&cNo owned claim with that id."));
return;
}
plugin.claimVisualService().show(player, claim.get());
player.sendMessage(plugin.messages().text("&aShowing claim #" + id + " boundary."));
}
private void appraise(Player player, String[] args) throws SQLException {
Optional<Claim> claim = resolveOwnedClaim(player, args, "appraise");
if (claim.isEmpty()) {
return;
}
LandAppraisalService.Appraisal appraisal = plugin.landAppraisalService().appraise(claim.get());
if (!appraisal.worldLoaded()) {
player.sendMessage(plugin.messages().text("&cThat claim's world is not loaded."));
return;
}
sendAppraisal(player, appraisal);
}
private void sell(Player player, String[] args) throws SQLException {
Optional<Claim> claim = resolveOwnedClaim(player, args, "sell");
if (claim.isEmpty()) {
return;
}
LandAppraisalService.Appraisal appraisal = plugin.landAppraisalService().appraise(claim.get());
if (!appraisal.worldLoaded()) {
player.sendMessage(plugin.messages().text("&cThat claim's world is not loaded."));
return;
}
ClaimService.SellResult result = plugin.claimService().sell(player.getUniqueId(), claim.get().id(), appraisal.totalValue());
if (!result.success()) {
player.sendMessage(plugin.messages().text("&c" + result.message()));
return;
}
plugin.claimMarkerService().removeMarkers(result.claim());
if (result.refund() >= 0L) {
player.sendMessage(plugin.messages().text("&aSold claim #" + result.claim().id() + " back to the Ministry for &f" + CoalMoney.format(result.refund()) + "&a."));
} else {
player.sendMessage(plugin.messages().text("&eSurrendered claim #" + result.claim().id() + " and paid &f" + CoalMoney.format(Math.abs(result.refund())) + " &efor extracted land value."));
}
sendAppraisal(player, appraisal);
}
private void transfer(Player player, String[] args) throws SQLException {
if (args.length != 3) {
player.sendMessage(plugin.messages().text("&eUsage: /claim transfer <id> <player>"));
return;
}
long id = parseLong(args[1]);
if (id <= 0L) {
player.sendMessage(plugin.messages().text("&cInvalid claim id."));
return;
}
OfflinePlayer recipient = Bukkit.getOfflinePlayer(args[2]);
plugin.economyService().ensurePlayer(recipient);
ClaimService.TransferResult result = plugin.claimService().transfer(player.getUniqueId(), id, recipient.getUniqueId());
if (!result.success()) {
player.sendMessage(plugin.messages().text("&c" + result.message()));
return;
}
String recipientName = recipient.getName() == null ? recipient.getUniqueId().toString() : recipient.getName();
player.sendMessage(plugin.messages().text("&aTransferred claim #" + id + " to &f" + recipientName + "&a."));
if (recipient.isOnline() && recipient.getPlayer() != null) {
recipient.getPlayer().sendMessage(plugin.messages().text("&a" + player.getName() + " transferred claim #" + id + " to you."));
}
}
private Optional<Claim> resolveOwnedClaim(Player player, String[] args, String action) throws SQLException {
if (args.length > 2) {
player.sendMessage(plugin.messages().text("&eUsage: /claim " + action + " [id]"));
return Optional.empty();
}
Optional<Claim> claim;
if (args.length == 2) {
long id = parseLong(args[1]);
if (id <= 0) {
player.sendMessage(plugin.messages().text("&cInvalid claim id."));
return Optional.empty();
}
claim = plugin.claimService().findOwnedById(player.getUniqueId(), id);
} else {
claim = plugin.claimService().claimAt(player.getLocation())
.filter(c -> c.ownerUuid().equals(player.getUniqueId()));
}
if (claim.isEmpty()) {
player.sendMessage(plugin.messages().text("&cStand inside one of your claims, or use /claim " + action + " <id>."));
}
return claim;
}
private void sendAppraisal(Player player, LandAppraisalService.Appraisal appraisal) {
player.sendMessage(plugin.messages().text("&eAppraisal: &f" + CoalMoney.format(appraisal.totalValue())));
player.sendMessage(plugin.messages().raw("&7Land condition: &f" + appraisal.conditionPercent()
+ "% &7base value: &f" + CoalMoney.format(Math.round(appraisal.conditionValue()))
+ " &7resources: &f" + CoalMoney.format(appraisal.resourceValue())
+ " &7depletion charge: &f" + CoalMoney.format(appraisal.depletionChargeRounded())));
if (appraisal.originalCost() == 0L) {
player.sendMessage(plugin.messages().raw("&7Starter homestead land value is &f0&7; only resources can offset depletion."));
}
String resourceSummary = resourceSummary(appraisal);
if (!resourceSummary.isEmpty()) {
player.sendMessage(plugin.messages().raw("&7Counted resources: &f" + resourceSummary));
}
}
private String resourceSummary(LandAppraisalService.Appraisal appraisal) {
StringBuilder builder = new StringBuilder();
int shown = 0;
for (var entry : appraisal.resources().entrySet()) {
if (entry.getValue() <= 0L) {
continue;
}
if (shown++ > 0) {
builder.append(", ");
}
builder.append(readableMaterial(entry.getKey())).append(" ").append(entry.getValue());
if (shown >= 5) {
break;
}
}
return builder.toString();
}
private String readableMaterial(Material material) {
return material.name().toLowerCase().replace('_', ' ');
}
private void abandon(Player player, String[] args) throws SQLException {
if (args.length != 2) {
player.sendMessage(plugin.messages().text("&eUsage: /claim abandon <id>"));
return;
}
long id = parseLong(args[1]);
if (id <= 0) {
player.sendMessage(plugin.messages().text("&cInvalid claim id."));
return;
}
Optional<Claim> claim = plugin.claimService().findOwnedById(player.getUniqueId(), id);
if (plugin.claimService().abandon(player.getUniqueId(), id)) {
claim.ifPresent(c -> plugin.claimMarkerService().removeMarkers(c));
player.sendMessage(plugin.messages().text("&aClaim abandoned."));
} else {
player.sendMessage(plugin.messages().text("&cNo owned claim with that id."));
}
}
private int parseInt(String raw) {
try {
return Integer.parseInt(raw);
} catch (NumberFormatException exception) {
return -1;
}
}
private long parseLong(String raw) {
try {
return Long.parseLong(raw);
} catch (NumberFormatException exception) {
return -1L;
}
}
}

View File

@@ -0,0 +1,205 @@
package com.librewiki.coalgov.command;
import com.librewiki.coalgov.CoalGovPlugin;
import com.librewiki.coalgov.model.Fine;
import com.librewiki.coalgov.util.CoalMoney;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import java.sql.SQLException;
import java.util.List;
public final class CoalCommand implements CommandExecutor {
private final CoalGovPlugin plugin;
public CoalCommand(CoalGovPlugin plugin) {
this.plugin = plugin;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage("Players only.");
return true;
}
if (args.length == 0) {
player.sendMessage(plugin.messages().text("&eUsage: /coal <balance|deposit|withdraw|pay|fines>"));
return true;
}
try {
plugin.economyService().ensurePlayer(player);
switch (args[0].toLowerCase()) {
case "balance" -> balance(player);
case "deposit" -> deposit(player);
case "withdraw" -> withdraw(player, args);
case "pay" -> pay(player, args);
case "fines" -> fines(player, args);
default -> player.sendMessage(plugin.messages().text("&eUsage: /coal <balance|deposit|withdraw|pay|fines>"));
}
} catch (SQLException exception) {
plugin.getLogger().warning("Coal command failed: " + exception.getMessage());
player.sendMessage(plugin.messages().text("&cCoal ledger unavailable."));
}
return true;
}
private void balance(Player player) throws SQLException {
player.sendMessage(plugin.messages().text("&eBalance: &f" + CoalMoney.format(plugin.economyService().balance(player.getUniqueId()))));
}
private void deposit(Player player) throws SQLException {
boolean coalEnabled = plugin.getConfig().getBoolean("economy.deposit_coal", true);
boolean blocksEnabled = plugin.getConfig().getBoolean("economy.deposit_coal_blocks", true);
long credit = 0;
for (ItemStack item : player.getInventory().getContents()) {
if (item == null) {
continue;
}
if (coalEnabled && item.getType() == Material.COAL) {
credit += item.getAmount() * CoalMoney.CENTS_PER_COAL;
item.setAmount(0);
} else if (blocksEnabled && item.getType() == Material.COAL_BLOCK) {
credit += item.getAmount() * 9L * CoalMoney.CENTS_PER_COAL;
item.setAmount(0);
}
}
if (credit <= 0) {
player.sendMessage(plugin.messages().text("&cNo depositable coal found."));
return;
}
plugin.economyService().deposit(player.getUniqueId(), credit);
player.sendMessage(plugin.messages().text("&aDeposited &f" + CoalMoney.format(credit) + "&a."));
}
private void withdraw(Player player, String[] args) throws SQLException {
if (args.length != 2) {
player.sendMessage(plugin.messages().text("&eUsage: /coal withdraw <amount>"));
return;
}
long amount = CoalMoney.parsePositive(args[1]);
if (amount <= 0) {
player.sendMessage(plugin.messages().text("&cAmount must be greater than zero."));
return;
}
if (amount % CoalMoney.CENTS_PER_COAL != 0L) {
player.sendMessage(plugin.messages().text("&cWithdrawals must be whole coal because they create coal items."));
return;
}
long coalItems = amount / CoalMoney.CENTS_PER_COAL;
if (plugin.economyService().balance(player.getUniqueId()) < amount) {
player.sendMessage(plugin.messages().text("&cBalance too low."));
return;
}
if (!canFitCoal(player, coalItems)) {
player.sendMessage(plugin.messages().text("&cYou need more inventory space."));
return;
}
if (!plugin.economyService().withdraw(player.getUniqueId(), amount)) {
player.sendMessage(plugin.messages().text("&cBalance too low."));
return;
}
long remaining = coalItems;
while (remaining > 0) {
int stack = (int) Math.min(Material.COAL.getMaxStackSize(), remaining);
player.getInventory().addItem(new ItemStack(Material.COAL, stack));
remaining -= stack;
}
player.sendMessage(plugin.messages().text("&aWithdrew &f" + CoalMoney.format(amount) + "&a."));
}
private void pay(Player player, String[] args) throws SQLException {
if (args.length != 3) {
player.sendMessage(plugin.messages().text("&eUsage: /coal pay <player> <amount>"));
return;
}
Player target = Bukkit.getPlayerExact(args[1]);
if (target == null) {
player.sendMessage(plugin.messages().text("&cThat player must be online."));
return;
}
long amount = CoalMoney.parsePositive(args[2]);
if (amount <= 0) {
player.sendMessage(plugin.messages().text("&cAmount must be greater than zero."));
return;
}
plugin.economyService().ensurePlayer(target);
if (!plugin.economyService().pay(player.getUniqueId(), target.getUniqueId(), amount)) {
player.sendMessage(plugin.messages().text("&cPayment failed. Check your balance."));
return;
}
player.sendMessage(plugin.messages().text("&aPaid &f" + CoalMoney.format(amount) + " &ato " + target.getName() + "."));
target.sendMessage(plugin.messages().text("&aReceived &f" + CoalMoney.format(amount) + " &afrom " + player.getName() + "."));
}
private void fines(Player player, String[] args) throws SQLException {
if (args.length == 1 || (args.length == 2 && args[1].equalsIgnoreCase("list"))) {
List<Fine> fines = plugin.governmentService().unpaidFines(player.getUniqueId());
if (fines.isEmpty()) {
player.sendMessage(plugin.messages().text("&aYou have no unpaid fines."));
return;
}
long total = fines.stream().mapToLong(Fine::amount).sum();
player.sendMessage(plugin.messages().text("&eUnpaid fines: &f" + CoalMoney.format(total) + " &etotal"));
for (Fine fine : fines) {
player.sendMessage(plugin.messages().raw("&7#" + fine.id() + " &f" + CoalMoney.format(fine.amount()) + " &7" + fine.reason()));
}
player.sendMessage(plugin.messages().text("&7Use &f/coal fines pay <id|all>&7."));
return;
}
if (args.length == 3 && args[1].equalsIgnoreCase("pay")) {
if (args[2].equalsIgnoreCase("all")) {
var result = plugin.governmentService().payAllFines(player.getUniqueId());
if (!result.success()) {
player.sendMessage(plugin.messages().text("&c" + result.message()));
return;
}
player.sendMessage(plugin.messages().text("&aPaid &f" + CoalMoney.format(result.amount()) + " &ain fines."));
return;
}
long id = parsePositive(args[2]);
if (id <= 0L) {
player.sendMessage(plugin.messages().text("&cInvalid fine id."));
return;
}
var result = plugin.governmentService().payFine(player.getUniqueId(), id);
if (!result.success()) {
player.sendMessage(plugin.messages().text("&c" + result.message()));
return;
}
player.sendMessage(plugin.messages().text("&aPaid fine #" + id + " for &f" + CoalMoney.format(result.amount()) + "&a."));
return;
}
player.sendMessage(plugin.messages().text("&eUsage: /coal fines [list|pay <id|all>]"));
}
private boolean canFitCoal(Player player, long amount) {
if (amount > Integer.MAX_VALUE) {
return false;
}
long capacity = 0;
for (ItemStack item : player.getInventory().getStorageContents()) {
if (item == null || item.getType().isAir()) {
capacity += Material.COAL.getMaxStackSize();
} else if (item.getType() == Material.COAL) {
capacity += Material.COAL.getMaxStackSize() - item.getAmount();
}
if (capacity >= amount) {
return true;
}
}
return false;
}
private long parsePositive(String raw) {
try {
return Long.parseLong(raw);
} catch (NumberFormatException exception) {
return -1L;
}
}
}

View File

@@ -0,0 +1,30 @@
package com.librewiki.coalgov.command;
import com.librewiki.coalgov.CoalGovPlugin;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
public final class DiviningRodCommand implements CommandExecutor {
private final CoalGovPlugin plugin;
public DiviningRodCommand(CoalGovPlugin plugin) {
this.plugin = plugin;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage("Players only.");
return true;
}
if (args.length != 0) {
player.sendMessage(plugin.messages().text("&eUsage: /diviningrod"));
return true;
}
player.getInventory().addItem(plugin.diviningRodService().rod());
player.sendMessage(plugin.messages().text("&aDivining rod granted."));
return true;
}
}

View File

@@ -0,0 +1,138 @@
package com.librewiki.coalgov.command;
import com.librewiki.coalgov.CoalGovPlugin;
import com.librewiki.coalgov.model.LandClass;
import com.librewiki.coalgov.model.LandRegion;
import com.librewiki.coalgov.util.Cuboid2D;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.sql.SQLException;
import java.util.List;
import java.util.Optional;
public final class LandCommand implements CommandExecutor {
private final CoalGovPlugin plugin;
public LandCommand(CoalGovPlugin plugin) {
this.plugin = plugin;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (args.length == 0) {
sender.sendMessage(plugin.messages().text("&eUsage: /land <info|create|delete|list>"));
return true;
}
try {
switch (args[0].toLowerCase()) {
case "info" -> info(sender);
case "create" -> create(sender, args);
case "delete" -> delete(sender, args);
case "list" -> list(sender);
default -> sender.sendMessage(plugin.messages().text("&eUsage: /land <info|create|delete|list>"));
}
} catch (SQLException exception) {
plugin.getLogger().warning("Land command failed: " + exception.getMessage());
sender.sendMessage(plugin.messages().text("&cLand registry unavailable."));
}
return true;
}
private void info(CommandSender sender) throws SQLException {
if (!(sender instanceof Player player)) {
sender.sendMessage("Players only.");
return;
}
if (!player.hasPermission("coalgov.land.info") && !player.hasPermission("coalgov.admin")) {
player.sendMessage(plugin.messages().text("&cYou lack permission."));
return;
}
Optional<LandRegion> region = plugin.landService().regionAt(player.getLocation());
if (region.isPresent()) {
LandRegion r = region.get();
player.sendMessage(plugin.messages().text("&eLand: &f" + r.name() + " " + r.landClass()
+ " [" + r.x1() + "," + r.z1() + "] to [" + r.x2() + "," + r.z2() + "]"));
} else {
player.sendMessage(plugin.messages().text("&eLand: &f" + plugin.landService().classAt(player.getLocation()) + " default"));
}
}
private void create(CommandSender sender, String[] args) throws SQLException {
if (!(sender instanceof Player player)) {
sender.sendMessage("Players only.");
return;
}
if (!player.hasPermission("coalgov.admin")) {
player.sendMessage(plugin.messages().text("&cYou lack permission."));
return;
}
if (args.length != 4) {
player.sendMessage(plugin.messages().text("&eUsage: /land create <name> <landClass> <radius>"));
return;
}
LandClass landClass;
try {
landClass = LandClass.valueOf(args[2].toUpperCase());
} catch (IllegalArgumentException exception) {
player.sendMessage(plugin.messages().text("&cInvalid land class."));
return;
}
int radius = parseInt(args[3]);
if (radius <= 0) {
player.sendMessage(plugin.messages().text("&cRadius must be greater than zero."));
return;
}
Cuboid2D area = new Cuboid2D(
player.getWorld().getName(),
player.getLocation().getBlockX() - radius,
player.getLocation().getBlockZ() - radius,
player.getLocation().getBlockX() + radius,
player.getLocation().getBlockZ() + radius
);
if (plugin.landService().create(args[1], area, landClass)) {
player.sendMessage(plugin.messages().text("&aLand region created."));
} else {
player.sendMessage(plugin.messages().text("&cCould not create land region."));
}
}
private void delete(CommandSender sender, String[] args) throws SQLException {
if (!sender.hasPermission("coalgov.admin")) {
sender.sendMessage(plugin.messages().text("&cYou lack permission."));
return;
}
if (args.length != 2) {
sender.sendMessage(plugin.messages().text("&eUsage: /land delete <name>"));
return;
}
if (plugin.landService().delete(args[1])) {
sender.sendMessage(plugin.messages().text("&aLand region deleted."));
} else {
sender.sendMessage(plugin.messages().text("&cNo land region with that name."));
}
}
private void list(CommandSender sender) throws SQLException {
List<LandRegion> regions = plugin.landService().listAll();
if (regions.isEmpty()) {
sender.sendMessage(plugin.messages().text("&eNo land regions."));
return;
}
sender.sendMessage(plugin.messages().text("&eLand regions:"));
for (LandRegion r : regions) {
sender.sendMessage(plugin.messages().raw("&7" + r.name() + " &f" + r.landClass() + " " + r.world()
+ " [" + r.x1() + "," + r.z1() + "] to [" + r.x2() + "," + r.z2() + "]"));
}
}
private int parseInt(String raw) {
try {
return Integer.parseInt(raw);
} catch (NumberFormatException exception) {
return -1;
}
}
}

View File

@@ -0,0 +1,33 @@
package com.librewiki.coalgov.command;
import com.librewiki.coalgov.CoalGovPlugin;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.sql.SQLException;
public final class MarketCommand implements CommandExecutor {
private final CoalGovPlugin plugin;
public MarketCommand(CoalGovPlugin plugin) {
this.plugin = plugin;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage("Players only.");
return true;
}
try {
plugin.economyService().ensurePlayer(player);
plugin.marketService().open(player);
} catch (SQLException exception) {
plugin.getLogger().warning("Market command failed: " + exception.getMessage());
player.sendMessage(plugin.messages().text("&cMarket ledger unavailable."));
}
return true;
}
}

View File

@@ -0,0 +1,216 @@
package com.librewiki.coalgov.command;
import com.librewiki.coalgov.CoalGovPlugin;
import com.librewiki.coalgov.model.ClaimPoint;
import com.librewiki.coalgov.model.CoalNpc;
import com.librewiki.coalgov.model.NpcZone;
import com.librewiki.coalgov.service.NpcService;
import com.librewiki.coalgov.service.OpenRouterService;
import com.librewiki.coalgov.util.CoalMoney;
import org.bukkit.Material;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.sql.SQLException;
import java.util.List;
import java.util.Optional;
public final class NpcCommand implements CommandExecutor {
private final CoalGovPlugin plugin;
public NpcCommand(CoalGovPlugin plugin) {
this.plugin = plugin;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!sender.hasPermission("coalgov.admin")) {
sender.sendMessage(plugin.messages().text("&cYou lack permission."));
return true;
}
try {
if (args.length >= 1 && args[0].equalsIgnoreCase("zone")) {
zone(sender, args);
} else if (args.length >= 1 && args[0].equalsIgnoreCase("create")) {
create(sender, args);
} else if (args.length >= 1 && args[0].equalsIgnoreCase("remove")) {
remove(sender, args);
} else if (args.length >= 1 && args[0].equalsIgnoreCase("list")) {
list(sender);
} else if (args.length >= 1 && args[0].equalsIgnoreCase("stock")) {
stock(sender, args);
} else if (args.length >= 1 && args[0].equalsIgnoreCase("funds")) {
funds(sender, args);
} else if (args.length >= 1 && args[0].equalsIgnoreCase("haggle")) {
haggle(sender, args);
} else {
usage(sender);
}
} catch (SQLException exception) {
plugin.getLogger().warning("NPC command failed: " + exception.getMessage());
sender.sendMessage(plugin.messages().text("&cNPC registry unavailable."));
}
return true;
}
private void zone(CommandSender sender, String[] args) throws SQLException {
if (!(sender instanceof Player player)) {
sender.sendMessage("Players only.");
return;
}
if (args.length != 3 || !args[1].equalsIgnoreCase("create")) {
player.sendMessage(plugin.messages().text("&eUsage: /cgnpc zone create <name>"));
return;
}
List<ClaimPoint> points = plugin.claimSelectionService().claimPoints(player);
if (points.size() < 3) {
player.sendMessage(plugin.messages().text("&cUse /claim wand to select at least 3 polygon points."));
return;
}
long id = plugin.npcService().createZone(args[2], player.getWorld().getName(), points);
plugin.claimSelectionService().consume(player);
player.sendMessage(plugin.messages().text("&aNPC zone #" + id + " created."));
}
private void create(CommandSender sender, String[] args) throws SQLException {
if (!(sender instanceof Player player)) {
sender.sendMessage("Players only.");
return;
}
if (args.length != 4) {
player.sendMessage(plugin.messages().text("&eUsage: /cgnpc create <trader|worker> <name> <zone>"));
return;
}
String type = args[1].toUpperCase();
if (!type.equals("TRADER") && !type.equals("WORKER")) {
player.sendMessage(plugin.messages().text("&cNPC type must be trader or worker."));
return;
}
Optional<NpcZone> zone = plugin.npcService().findZone(args[3]);
if (zone.isEmpty()) {
player.sendMessage(plugin.messages().text("&cNo NPC zone named " + args[3] + "."));
return;
}
long id = plugin.npcService().createNpc(type, args[2], zone.get(), player.getLocation());
player.sendMessage(plugin.messages().text("&aNPC #" + id + " created."));
}
private void remove(CommandSender sender, String[] args) throws SQLException {
if (args.length != 2) {
sender.sendMessage(plugin.messages().text("&eUsage: /cgnpc remove <id>"));
return;
}
long id = parseLong(args[1]);
if (plugin.npcService().removeNpc(id)) {
sender.sendMessage(plugin.messages().text("&aNPC removed."));
} else {
sender.sendMessage(plugin.messages().text("&cNo NPC with that id."));
}
}
private void list(CommandSender sender) throws SQLException {
sender.sendMessage(plugin.messages().text("&eNPC zones:"));
for (NpcZone zone : plugin.npcService().listZones()) {
sender.sendMessage(plugin.messages().raw("&7#" + zone.id() + " &f" + zone.name() + " " + zone.world()));
}
sender.sendMessage(plugin.messages().text("&eNPCs:"));
for (CoalNpc npc : plugin.npcService().listNpcs()) {
sender.sendMessage(plugin.messages().raw("&7#" + npc.id() + " &f" + npc.name() + " " + npc.npcType() + " citizens " + npc.citizensId()));
}
}
private void stock(CommandSender sender, String[] args) throws SQLException {
if (args.length != 4) {
sender.sendMessage(plugin.messages().text("&eUsage: /cgnpc stock <id> <material> <amount>"));
return;
}
Optional<CoalNpc> npc = plugin.npcService().findNpc(parseLong(args[1]));
Material material = Material.matchMaterial(args[2]);
long amount = parseLong(args[3]);
if (npc.isEmpty() || material == null || amount <= 0L) {
sender.sendMessage(plugin.messages().text("&cInvalid NPC, material, or amount."));
return;
}
plugin.npcService().addInventory(npc.get().id(), material, amount);
sender.sendMessage(plugin.messages().text("&aNPC stock added."));
}
private void funds(CommandSender sender, String[] args) throws SQLException {
if (args.length != 3) {
sender.sendMessage(plugin.messages().text("&eUsage: /cgnpc funds <id> <amount>"));
return;
}
Optional<CoalNpc> npc = plugin.npcService().findNpc(parseLong(args[1]));
long amount = CoalMoney.parsePositive(args[2]);
if (npc.isEmpty() || amount <= 0L) {
sender.sendMessage(plugin.messages().text("&cInvalid NPC or amount."));
return;
}
plugin.economyService().credit(npc.get().accountUuid(), amount, "npc_admin_funds");
sender.sendMessage(plugin.messages().text("&aNPC funded."));
}
private void haggle(CommandSender sender, String[] args) throws SQLException {
if (!(sender instanceof Player player)) {
sender.sendMessage("Players only.");
return;
}
if (args.length != 6) {
player.sendMessage(plugin.messages().text("&eUsage: /cgnpc haggle <id> <buy|sell> <material> <amount> <offer>"));
return;
}
Optional<CoalNpc> npc = plugin.npcService().findNpc(parseLong(args[1]));
Material material = Material.matchMaterial(args[3]);
int amount = (int) parseLong(args[4]);
long offer = CoalMoney.parsePositive(args[5]);
if (npc.isEmpty() || !npc.get().trader() || material == null || amount <= 0 || offer <= 0L) {
player.sendMessage(plugin.messages().text("&cInvalid trader, material, amount, or offer."));
return;
}
NpcService.HaggleQuote quote = plugin.npcService().quote(npc.get(), args[2], material, amount, offer);
if (!quote.reason().isEmpty()) {
player.sendMessage(plugin.messages().text("&c" + quote.reason()));
return;
}
String prompt = "NPC " + npc.get().name() + " is haggling. Mode " + quote.mode()
+ ", material " + material.name() + ", amount " + amount
+ ", player offer " + CoalMoney.format(offer) + " (" + offer + " coal cents), acceptable range "
+ CoalMoney.format(quote.floor()) + " to " + CoalMoney.format(quote.ceiling()) + " (" + quote.floor() + " to " + quote.ceiling() + " coal cents)"
+ ", deterministic counter " + CoalMoney.format(quote.counter()) + " (" + quote.counter() + " coal cents). Reply with JSON using counter as an integer coal-cent amount.";
player.sendMessage(plugin.messages().text("&7" + npc.get().name() + " considers the offer..."));
plugin.openRouterService().haggle(prompt, quote.floor(), quote.ceiling()).thenAccept(ai -> plugin.getServer().getScheduler().runTask(plugin, () -> {
try {
long finalPrice = ai.map(OpenRouterService.HaggleAiResult::counter).orElse(quote.counter());
boolean accepted = quote.accepted() || ai.map(OpenRouterService.HaggleAiResult::accept).orElse(false) && offer == finalPrice;
String line = ai.map(OpenRouterService.HaggleAiResult::message)
.orElse(accepted ? "Deal." : "I can do " + CoalMoney.format(finalPrice) + ".");
player.sendMessage(plugin.messages().text("&e" + npc.get().name() + ": &f" + line));
if (accepted) {
if (plugin.npcService().completeTrade(player, npc.get(), quote, offer)) {
player.sendMessage(plugin.messages().text("&aTrade complete for &f" + CoalMoney.format(offer) + "&a."));
} else {
player.sendMessage(plugin.messages().text("&cTrade failed. Check balance, stock, and inventory."));
}
} else {
player.sendMessage(plugin.messages().text("&7Counter offer: &f" + CoalMoney.format(finalPrice) + "&7. Run the haggle command again with that offer to accept."));
}
} catch (SQLException exception) {
player.sendMessage(plugin.messages().text("&cNPC trade failed."));
}
}));
}
private void usage(CommandSender sender) {
sender.sendMessage(plugin.messages().text("&eUsage: /cgnpc <zone|create|remove|list|stock|funds|haggle>"));
}
private long parseLong(String raw) {
try {
return Long.parseLong(raw);
} catch (NumberFormatException exception) {
return -1L;
}
}
}

View File

@@ -0,0 +1,74 @@
package com.librewiki.coalgov.command;
import com.librewiki.coalgov.CoalGovPlugin;
import com.librewiki.coalgov.model.Permit;
import com.librewiki.coalgov.service.PermitService;
import com.librewiki.coalgov.util.CoalMoney;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.sql.SQLException;
import java.time.Instant;
import java.util.List;
public final class PermitCommand implements CommandExecutor {
private final CoalGovPlugin plugin;
public PermitCommand(CoalGovPlugin plugin) {
this.plugin = plugin;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage("Players only.");
return true;
}
if (args.length == 0) {
player.sendMessage(plugin.messages().text("&eUsage: /permit <buy|list>"));
return true;
}
try {
plugin.economyService().ensurePlayer(player);
switch (args[0].toLowerCase()) {
case "buy" -> buy(player, args);
case "list" -> list(player);
default -> player.sendMessage(plugin.messages().text("&eUsage: /permit <buy|list>"));
}
} catch (SQLException exception) {
plugin.getLogger().warning("Permit command failed: " + exception.getMessage());
player.sendMessage(plugin.messages().text("&cPermit registry unavailable."));
}
return true;
}
private void buy(Player player, String[] args) throws SQLException {
if (args.length != 3 || !args[1].equalsIgnoreCase("mining")) {
player.sendMessage(plugin.messages().text("&eUsage: /permit buy mining <regionName>"));
return;
}
PermitService.BuyResult result = plugin.permitService().buyMining(player.getUniqueId(), args[2]);
if (!result.success()) {
player.sendMessage(plugin.messages().text("&c" + result.message()));
return;
}
player.sendMessage(plugin.messages().text("&aMining permit #" + result.id() + " bought for &f"
+ CoalMoney.format(result.cost()) + "&a. Expires " + Instant.ofEpochMilli(result.expiresAt()) + "."));
}
private void list(Player player) throws SQLException {
List<Permit> permits = plugin.permitService().listActive(player.getUniqueId());
if (permits.isEmpty()) {
player.sendMessage(plugin.messages().text("&eYou have no active permits."));
return;
}
player.sendMessage(plugin.messages().text("&eActive permits:"));
for (Permit permit : permits) {
String expiry = permit.expiresAt() == null ? "never" : Instant.ofEpochMilli(permit.expiresAt()).toString();
player.sendMessage(plugin.messages().raw("&7#" + permit.id() + " &f" + permit.permitType()
+ " " + (permit.regionName() == null ? "global" : permit.regionName()) + " expires " + expiry));
}
}
}

View File

@@ -0,0 +1,67 @@
package com.librewiki.coalgov.command;
import com.librewiki.coalgov.CoalGovPlugin;
import com.librewiki.coalgov.util.CoalMoney;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.UUID;
public final class PoliceCommand implements CommandExecutor {
private final CoalGovPlugin plugin;
public PoliceCommand(CoalGovPlugin plugin) {
this.plugin = plugin;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!sender.hasPermission("coalgov.police")) {
sender.sendMessage(plugin.messages().text("&cYou lack permission."));
return true;
}
try {
if (args.length >= 1 && args[0].equalsIgnoreCase("fine")) {
fine(sender, args);
return true;
}
usage(sender);
} catch (SQLException exception) {
plugin.getLogger().warning("Police command failed: " + exception.getMessage());
sender.sendMessage(plugin.messages().text("&cPolice records unavailable."));
}
return true;
}
private void fine(CommandSender sender, String[] args) throws SQLException {
if (args.length < 4) {
sender.sendMessage(plugin.messages().text("&eUsage: /police fine <player> <amount> <reason>"));
return;
}
OfflinePlayer target = Bukkit.getOfflinePlayer(args[1]);
plugin.economyService().ensurePlayer(target);
long amount = CoalMoney.parsePositive(args[2]);
if (amount <= 0L) {
sender.sendMessage(plugin.messages().text("&cAmount must be greater than zero."));
return;
}
String reason = String.join(" ", Arrays.copyOfRange(args, 3, args.length));
UUID issuer = sender instanceof Player player ? player.getUniqueId() : null;
long id = plugin.governmentService().createFine(target.getUniqueId(), issuer, amount, reason);
sender.sendMessage(plugin.messages().text("&aFine #" + id + " issued to &f" + args[1] + " &afor &f" + CoalMoney.format(amount) + "&a."));
if (target.isOnline() && target.getPlayer() != null) {
target.getPlayer().sendMessage(plugin.messages().text("&cFine #" + id + ": &f" + CoalMoney.format(amount) + " &cfor " + reason + ". Use /coal fines pay " + id + "."));
}
}
private void usage(CommandSender sender) {
sender.sendMessage(plugin.messages().text("&eUsage: /police fine <player> <amount> <reason>"));
}
}

View File

@@ -0,0 +1,48 @@
package com.librewiki.coalgov.listener;
import com.librewiki.coalgov.CoalGovPlugin;
import com.librewiki.coalgov.service.BuildProtectionService;
import org.bukkit.Material;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
import java.sql.SQLException;
public final class BlockBreakListener implements Listener {
private final CoalGovPlugin plugin;
public BlockBreakListener(CoalGovPlugin plugin) {
this.plugin = plugin;
}
@EventHandler(ignoreCancelled = true)
public void onBlockBreak(BlockBreakEvent event) {
Material type = event.getBlock().getType();
try {
if (!plugin.hasAdminBypass(event.getPlayer()) && plugin.claimMarkerService().isMarkerBlock(event.getBlock())) {
event.setCancelled(true);
event.getPlayer().sendMessage(plugin.messages().text("&cClaim corner markers are protected."));
return;
}
BuildProtectionService.BuildDecision decision = plugin.buildProtectionService()
.canModify(event.getPlayer(), event.getBlock().getLocation());
if (!decision.allowed()) {
event.setCancelled(true);
event.getPlayer().sendMessage(plugin.messages().text("&c" + decision.message()));
return;
}
if ((type == Material.COAL_ORE || type == Material.DEEPSLATE_COAL_ORE)
&& !plugin.miningService().canMineCoal(event.getPlayer(), event.getBlock().getLocation())) {
event.setCancelled(true);
event.getPlayer().sendMessage(plugin.messages().text("&cMining denied: this coal seam is regulated by the Ministry."));
}
} catch (SQLException exception) {
event.setCancelled(true);
plugin.getLogger().warning("Block break check failed: " + exception.getMessage());
event.getPlayer().sendMessage(plugin.messages().text("&cLand registry unavailable. Try again later."));
}
}
}

View File

@@ -0,0 +1,39 @@
package com.librewiki.coalgov.listener;
import com.librewiki.coalgov.CoalGovPlugin;
import com.librewiki.coalgov.service.BuildProtectionService;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockPlaceEvent;
import java.sql.SQLException;
public final class BlockPlaceListener implements Listener {
private final CoalGovPlugin plugin;
public BlockPlaceListener(CoalGovPlugin plugin) {
this.plugin = plugin;
}
@EventHandler(ignoreCancelled = true)
public void onBlockPlace(BlockPlaceEvent event) {
try {
if (!plugin.hasAdminBypass(event.getPlayer()) && plugin.claimMarkerService().isMarkerColumn(event.getBlockPlaced())) {
event.setCancelled(true);
event.getPlayer().sendMessage(plugin.messages().text("&cClaim corner markers cannot be covered."));
return;
}
BuildProtectionService.BuildDecision decision = plugin.buildProtectionService()
.canModify(event.getPlayer(), event.getBlockPlaced().getLocation());
if (!decision.allowed()) {
event.setCancelled(true);
event.getPlayer().sendMessage(plugin.messages().text("&c" + decision.message()));
}
} catch (SQLException exception) {
event.setCancelled(true);
plugin.getLogger().warning("Build protection check failed: " + exception.getMessage());
event.getPlayer().sendMessage(plugin.messages().text("&cLand registry unavailable. Try again later."));
}
}
}

View File

@@ -0,0 +1,31 @@
package com.librewiki.coalgov.listener;
import com.librewiki.coalgov.CoalGovPlugin;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.player.PlayerInteractEvent;
public final class ClaimWandListener implements Listener {
private final CoalGovPlugin plugin;
public ClaimWandListener(CoalGovPlugin plugin) {
this.plugin = plugin;
}
@EventHandler
public void onPlayerInteract(PlayerInteractEvent event) {
if (!plugin.claimSelectionService().isWand(event.getItem())) {
return;
}
event.setCancelled(true);
if (event.getAction() != Action.RIGHT_CLICK_BLOCK || event.getClickedBlock() == null) {
return;
}
if (event.getPlayer().isSneaking()) {
plugin.claimSelectionService().clear(event.getPlayer());
return;
}
plugin.claimSelectionService().addPoint(event.getPlayer(), event.getClickedBlock().getLocation());
}
}

View File

@@ -0,0 +1,27 @@
package com.librewiki.coalgov.listener;
import com.librewiki.coalgov.CoalGovPlugin;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.player.PlayerInteractEvent;
public final class DiviningRodListener implements Listener {
private final CoalGovPlugin plugin;
public DiviningRodListener(CoalGovPlugin plugin) {
this.plugin = plugin;
}
@EventHandler
public void onPlayerInteract(PlayerInteractEvent event) {
if (!plugin.diviningRodService().isRod(event.getItem())) {
return;
}
event.setCancelled(true);
if (event.getAction() != Action.RIGHT_CLICK_AIR && event.getAction() != Action.RIGHT_CLICK_BLOCK) {
return;
}
plugin.diviningRodService().scan(event.getPlayer());
}
}

View File

@@ -0,0 +1,54 @@
package com.librewiki.coalgov.listener;
import com.librewiki.coalgov.CoalGovPlugin;
import com.librewiki.coalgov.service.MarketService;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.ClickType;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.inventory.ItemStack;
import java.sql.SQLException;
public final class MarketListener implements Listener {
private final CoalGovPlugin plugin;
public MarketListener(CoalGovPlugin plugin) {
this.plugin = plugin;
}
@EventHandler
public void onInventoryClick(InventoryClickEvent event) {
if (!MarketService.TITLE.equals(event.getView().getTitle())) {
return;
}
event.setCancelled(true);
if (!(event.getWhoClicked() instanceof Player player)) {
return;
}
try {
if (plugin.marketService().isSellAllSlot(event.getRawSlot())) {
plugin.marketService().sellAll(player);
player.closeInventory();
return;
}
ItemStack clicked = event.getCurrentItem();
if (clicked == null || clicked.getType() == Material.AIR) {
return;
}
plugin.marketService().buy(player, clicked.getType(), purchaseAmount(event.getClick()));
} catch (SQLException exception) {
plugin.getLogger().warning("Market click failed: " + exception.getMessage());
player.sendMessage(plugin.messages().text("&cMarket ledger unavailable."));
}
}
private int purchaseAmount(ClickType clickType) {
if (clickType.isShiftClick()) {
return 64;
}
return 1;
}
}

View File

@@ -0,0 +1,61 @@
package com.librewiki.coalgov.listener;
import com.librewiki.coalgov.CoalGovPlugin;
import com.librewiki.coalgov.model.CoalNpc;
import com.librewiki.coalgov.util.CoalMoney;
import net.citizensnpcs.api.event.NPCRightClickEvent;
import org.bukkit.Material;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import java.sql.SQLException;
import java.util.Map;
public final class NpcListener implements Listener {
private final CoalGovPlugin plugin;
public NpcListener(CoalGovPlugin plugin) {
this.plugin = plugin;
}
@EventHandler
public void onNpcRightClick(NPCRightClickEvent event) {
try {
var coalNpc = plugin.npcService().findByCitizensId(event.getNPC().getId());
if (coalNpc.isEmpty()) {
return;
}
event.setCancelled(true);
showNpc(event.getClicker(), coalNpc.get());
} catch (SQLException exception) {
plugin.getLogger().warning("NPC click failed: " + exception.getMessage());
event.getClicker().sendMessage(plugin.messages().text("&cNPC registry unavailable."));
}
}
private void showNpc(org.bukkit.entity.Player player, CoalNpc npc) throws SQLException {
player.sendMessage(plugin.messages().text("&e" + npc.name() + " &7(" + npc.npcType().toLowerCase() + " #" + npc.id() + ")"));
player.sendMessage(plugin.messages().text("&7Balance: &f" + CoalMoney.format(plugin.npcService().balance(npc))));
Map<Material, Long> inventory = plugin.npcService().inventory(npc.id());
if (inventory.isEmpty()) {
player.sendMessage(plugin.messages().text("&7Inventory: &fempty"));
} else {
StringBuilder builder = new StringBuilder();
int shown = 0;
for (var entry : inventory.entrySet()) {
if (shown++ > 0) {
builder.append(", ");
}
builder.append(entry.getKey().name().toLowerCase()).append(" ").append(entry.getValue());
if (shown >= 6) {
break;
}
}
player.sendMessage(plugin.messages().text("&7Inventory: &f" + builder));
}
if (npc.trader()) {
player.sendMessage(plugin.messages().text("&7Haggle: &f/cgnpc haggle " + npc.id() + " buy <material> <amount> <offer>"));
player.sendMessage(plugin.messages().text("&7Or sell: &f/cgnpc haggle " + npc.id() + " sell <material> <amount> <offer>"));
}
}
}

View File

@@ -0,0 +1,25 @@
package com.librewiki.coalgov.listener;
import com.librewiki.coalgov.CoalGovPlugin;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import java.sql.SQLException;
public final class PlayerJoinListener implements Listener {
private final CoalGovPlugin plugin;
public PlayerJoinListener(CoalGovPlugin plugin) {
this.plugin = plugin;
}
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
try {
plugin.economyService().ensurePlayer(event.getPlayer());
} catch (SQLException exception) {
plugin.getLogger().warning("Failed to ensure player row: " + exception.getMessage());
}
}
}

View File

@@ -0,0 +1,74 @@
package com.librewiki.coalgov.listener;
import com.librewiki.coalgov.CoalGovPlugin;
import org.bukkit.Material;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.inventory.FurnaceBurnEvent;
import org.bukkit.event.inventory.FurnaceStartSmeltEvent;
import java.sql.SQLException;
public final class SuperFurnaceListener implements Listener {
private final CoalGovPlugin plugin;
public SuperFurnaceListener(CoalGovPlugin plugin) {
this.plugin = plugin;
}
@EventHandler(ignoreCancelled = true)
public void onBlockPlace(BlockPlaceEvent event) {
if (event.getBlockPlaced().getType() != Material.FURNACE
|| !plugin.superFurnaceService().isItem(event.getItemInHand())) {
return;
}
try {
plugin.superFurnaceService().add(event.getBlockPlaced());
event.getPlayer().sendMessage(plugin.messages().text("&aSuper furnace installed."));
} catch (SQLException exception) {
event.setCancelled(true);
plugin.getLogger().warning("Super furnace placement failed: " + exception.getMessage());
event.getPlayer().sendMessage(plugin.messages().text("&cFurnace registry unavailable."));
}
}
@EventHandler(ignoreCancelled = true)
public void onBlockBreak(BlockBreakEvent event) {
try {
if (!plugin.superFurnaceService().isSuperFurnace(event.getBlock())) {
return;
}
plugin.superFurnaceService().remove(event.getBlock());
event.setDropItems(false);
event.getBlock().getWorld().dropItemNaturally(event.getBlock().getLocation(), plugin.superFurnaceService().item());
} catch (SQLException exception) {
event.setCancelled(true);
plugin.getLogger().warning("Super furnace break failed: " + exception.getMessage());
event.getPlayer().sendMessage(plugin.messages().text("&cFurnace registry unavailable."));
}
}
@EventHandler(ignoreCancelled = true)
public void onFurnaceStartSmelt(FurnaceStartSmeltEvent event) {
try {
if (plugin.superFurnaceService().isSuperFurnace(event.getBlock())) {
event.setTotalCookTime(plugin.superFurnaceService().fasterCookTime(event.getTotalCookTime()));
}
} catch (SQLException exception) {
plugin.getLogger().warning("Super furnace smelt check failed: " + exception.getMessage());
}
}
@EventHandler(ignoreCancelled = true)
public void onFurnaceBurn(FurnaceBurnEvent event) {
try {
if (plugin.superFurnaceService().isSuperFurnace(event.getBlock())) {
event.setBurnTime(plugin.superFurnaceService().shorterBurnTime(event.getBurnTime()));
}
} catch (SQLException exception) {
plugin.getLogger().warning("Super furnace burn check failed: " + exception.getMessage());
}
}
}

View File

@@ -0,0 +1,65 @@
package com.librewiki.coalgov.model;
import java.util.UUID;
import java.util.List;
public record Claim(
long id,
UUID ownerUuid,
String world,
int x1,
int z1,
int x2,
int z2,
String claimType,
Long expiresAt,
long taxDue,
long createdAt,
long purchaseCost,
List<ClaimPoint> vertices
) {
public Claim(
long id,
UUID ownerUuid,
String world,
int x1,
int z1,
int x2,
int z2,
String claimType,
Long expiresAt,
long taxDue,
long createdAt
) {
this(id, ownerUuid, world, x1, z1, x2, z2, claimType, expiresAt, taxDue, createdAt, -1L, List.of());
}
public boolean contains(String worldName, int x, int z) {
if (!world.equals(worldName) || x < x1 || x > x2 || z < z1 || z > z2) {
return false;
}
if (vertices == null || vertices.size() < 3) {
return true;
}
return containsPolygon(x + 0.5D, z + 0.5D);
}
public boolean isPolygon() {
return vertices != null && vertices.size() >= 3;
}
private boolean containsPolygon(double x, double z) {
boolean inside = false;
int size = vertices.size();
for (int i = 0, j = size - 1; i < size; j = i++) {
ClaimPoint a = vertices.get(i);
ClaimPoint b = vertices.get(j);
boolean intersects = ((a.z() > z) != (b.z() > z))
&& (x < (double) (b.x() - a.x()) * (z - a.z()) / (double) (b.z() - a.z()) + a.x());
if (intersects) {
inside = !inside;
}
}
return inside;
}
}

View File

@@ -0,0 +1,4 @@
package com.librewiki.coalgov.model;
public record ClaimPoint(int x, int z) {
}

View File

@@ -0,0 +1,24 @@
package com.librewiki.coalgov.model;
import java.util.UUID;
public record CoalNpc(
long id,
int citizensId,
UUID accountUuid,
String npcType,
String name,
long zoneId,
String world,
double x,
double y,
double z
) {
public boolean trader() {
return npcType.equalsIgnoreCase("TRADER");
}
public boolean worker() {
return npcType.equalsIgnoreCase("WORKER");
}
}

View File

@@ -0,0 +1,13 @@
package com.librewiki.coalgov.model;
import java.util.UUID;
public record CoalTransaction(
long id,
UUID fromUuid,
UUID toUuid,
long amount,
String reason,
long createdAt
) {
}

View File

@@ -0,0 +1,17 @@
package com.librewiki.coalgov.model;
import java.util.UUID;
public record Fine(
long id,
UUID playerUuid,
UUID issuerUuid,
long amount,
String reason,
Long paidAt,
long createdAt
) {
public boolean paid() {
return paidAt != null;
}
}

View File

@@ -0,0 +1,11 @@
package com.librewiki.coalgov.model;
public enum LandClass {
FREEHOLD,
LEASE,
GOVERNMENT,
PUBLIC,
MINING_CONCESSION,
PROTECTED_PRESERVE,
BORDER_ZONE
}

View File

@@ -0,0 +1,17 @@
package com.librewiki.coalgov.model;
public record LandRegion(
long id,
String name,
String world,
int x1,
int z1,
int x2,
int z2,
LandClass landClass,
long createdAt
) {
public boolean contains(String worldName, int x, int z) {
return world.equals(worldName) && x >= x1 && x <= x2 && z >= z1 && z <= z2;
}
}

View File

@@ -0,0 +1,37 @@
package com.librewiki.coalgov.model;
import java.util.List;
public record NpcZone(
long id,
String name,
String world,
int x1,
int z1,
int x2,
int z2,
List<ClaimPoint> vertices
) {
public boolean contains(String worldName, int x, int z) {
if (!world.equals(worldName) || x < x1 || x > x2 || z < z1 || z > z2) {
return false;
}
if (vertices == null || vertices.size() < 3) {
return true;
}
boolean inside = false;
int size = vertices.size();
double px = x + 0.5D;
double pz = z + 0.5D;
for (int i = 0, j = size - 1; i < size; j = i++) {
ClaimPoint a = vertices.get(i);
ClaimPoint b = vertices.get(j);
boolean intersects = ((a.z() > pz) != (b.z() > pz))
&& (px < (double) (b.x() - a.x()) * (pz - a.z()) / (double) (b.z() - a.z()) + a.x());
if (intersects) {
inside = !inside;
}
}
return inside;
}
}

View File

@@ -0,0 +1,16 @@
package com.librewiki.coalgov.model;
import java.util.UUID;
public record Permit(
long id,
UUID ownerUuid,
PermitType permitType,
String regionName,
Long expiresAt,
long createdAt
) {
public boolean isActive(long now) {
return expiresAt == null || expiresAt > now;
}
}

View File

@@ -0,0 +1,7 @@
package com.librewiki.coalgov.model;
public enum PermitType {
MINING,
BORDER_PASS,
GOVERNMENT_CONTRACTOR
}

View File

@@ -0,0 +1,43 @@
package com.librewiki.coalgov.service;
import com.librewiki.coalgov.CoalGovPlugin;
import com.librewiki.coalgov.model.Claim;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import java.sql.SQLException;
import java.util.Optional;
public final class BuildProtectionService {
private final CoalGovPlugin plugin;
private final ClaimService claimService;
public BuildProtectionService(CoalGovPlugin plugin, ClaimService claimService) {
this.plugin = plugin;
this.claimService = claimService;
}
public BuildDecision canModify(Player player, Location location) throws SQLException {
if (plugin.hasAdminBypass(player)) {
return BuildDecision.allow();
}
Optional<Claim> claim = claimService.claimAt(location);
if (claim.isEmpty()) {
return BuildDecision.deny("This land is unclaimed and protected by the Ministry.");
}
if (!claim.get().ownerUuid().equals(player.getUniqueId())) {
return BuildDecision.deny("This claim belongs to another citizen.");
}
return BuildDecision.allow();
}
public record BuildDecision(boolean allowed, String message) {
public static BuildDecision allow() {
return new BuildDecision(true, "");
}
public static BuildDecision deny(String message) {
return new BuildDecision(false, message);
}
}
}

View File

@@ -0,0 +1,103 @@
package com.librewiki.coalgov.service;
import com.librewiki.coalgov.CoalGovPlugin;
import com.librewiki.coalgov.model.Claim;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Block;
import java.sql.SQLException;
public final class ClaimMarkerService {
private static final Material MARKER_MATERIAL = Material.GOLD_BLOCK;
private final CoalGovPlugin plugin;
private final ClaimService claimService;
public ClaimMarkerService(CoalGovPlugin plugin, ClaimService claimService) {
this.plugin = plugin;
this.claimService = claimService;
}
public void placeMarkers(Claim claim) {
World world = plugin.getServer().getWorld(claim.world());
if (world == null) {
return;
}
if (claim.isPolygon()) {
for (var point : claim.vertices()) {
placeMarker(world, point.x(), point.z());
}
return;
}
placeMarker(world, claim.x1(), claim.z1());
placeMarker(world, claim.x1(), claim.z2());
placeMarker(world, claim.x2(), claim.z1());
placeMarker(world, claim.x2(), claim.z2());
}
public void removeMarkers(Claim claim) {
World world = plugin.getServer().getWorld(claim.world());
if (world == null) {
return;
}
if (claim.isPolygon()) {
for (var point : claim.vertices()) {
removeMarkerColumn(world, point.x(), point.z());
}
return;
}
removeMarkerColumn(world, claim.x1(), claim.z1());
removeMarkerColumn(world, claim.x1(), claim.z2());
removeMarkerColumn(world, claim.x2(), claim.z1());
removeMarkerColumn(world, claim.x2(), claim.z2());
}
public void placeAllLoadedMarkers() {
try {
for (Claim claim : claimService.listAll()) {
placeMarkers(claim);
}
} catch (SQLException exception) {
plugin.getLogger().warning("Failed to place claim markers: " + exception.getMessage());
}
}
public boolean isMarkerBlock(Block block) throws SQLException {
if (block.getType() != MARKER_MATERIAL) {
return false;
}
return isMarkerColumn(block);
}
public boolean isMarkerColumn(Block block) throws SQLException {
return claimService.claimAt(block.getLocation())
.map(claim -> isCorner(claim, block.getX(), block.getZ()))
.orElse(false);
}
private void placeMarker(World world, int x, int z) {
int y = world.getHighestBlockYAt(x, z);
Block highest = world.getBlockAt(x, y, z);
if (highest.getType() == MARKER_MATERIAL) {
return;
}
world.getBlockAt(x, y + 1, z).setType(MARKER_MATERIAL, false);
}
private void removeMarkerColumn(World world, int x, int z) {
for (int y = world.getMinHeight(); y < world.getMaxHeight(); y++) {
Block block = world.getBlockAt(x, y, z);
if (block.getType() == MARKER_MATERIAL) {
block.setType(Material.AIR, false);
}
}
}
private boolean isCorner(Claim claim, int x, int z) {
if (claim.isPolygon()) {
return claim.vertices().stream().anyMatch(point -> point.x() == x && point.z() == z);
}
return (x == claim.x1() || x == claim.x2()) && (z == claim.z1() || z == claim.z2());
}
}

View File

@@ -0,0 +1,166 @@
package com.librewiki.coalgov.service;
import com.librewiki.coalgov.CoalGovPlugin;
import com.librewiki.coalgov.model.ClaimPoint;
import org.bukkit.Color;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.Particle;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.persistence.PersistentDataType;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
public final class ClaimSelectionService {
private final CoalGovPlugin plugin;
private final NamespacedKey wandKey;
private final Map<UUID, List<Location>> selections = new HashMap<>();
private final Map<UUID, List<Location>> markerPreviews = new HashMap<>();
public ClaimSelectionService(CoalGovPlugin plugin) {
this.plugin = plugin;
this.wandKey = new NamespacedKey(plugin, "claim_wand");
}
public ItemStack wand() {
ItemStack item = new ItemStack(Material.BLAZE_ROD);
ItemMeta meta = item.getItemMeta();
meta.setDisplayName(plugin.messages().raw("&6CoalGov Claim Wand"));
meta.setLore(List.of(
plugin.messages().raw("&7Right-click blocks to add polygon points."),
plugin.messages().raw("&7Sneak right-click to clear.")
));
meta.getPersistentDataContainer().set(wandKey, PersistentDataType.BYTE, (byte) 1);
item.setItemMeta(meta);
return item;
}
public boolean isWand(ItemStack item) {
if (item == null || item.getType() != Material.BLAZE_ROD || !item.hasItemMeta()) {
return false;
}
return item.getItemMeta().getPersistentDataContainer().has(wandKey, PersistentDataType.BYTE);
}
public void addPoint(Player player, Location clicked) {
List<Location> points = selections.computeIfAbsent(player.getUniqueId(), ignored -> new ArrayList<>());
if (!points.isEmpty() && !points.get(0).getWorld().equals(clicked.getWorld())) {
restoreMarkerPreviews(player);
points.clear();
}
points.add(clicked.toBlockLocation());
player.sendMessage(plugin.messages().text("&aPolygon point " + points.size() + " set at "
+ clicked.getBlockX() + ", " + clicked.getBlockZ() + "."));
show(player);
}
public void clear(Player player) {
restoreMarkerPreviews(player);
selections.remove(player.getUniqueId());
player.sendMessage(plugin.messages().text("&aClaim polygon cleared."));
}
public List<Location> points(Player player) {
return selections.getOrDefault(player.getUniqueId(), List.of());
}
public List<ClaimPoint> claimPoints(Player player) {
List<ClaimPoint> points = new ArrayList<>();
for (Location location : points(player)) {
points.add(new ClaimPoint(location.getBlockX(), location.getBlockZ()));
}
return points;
}
public void consume(Player player) {
restoreMarkerPreviews(player);
selections.remove(player.getUniqueId());
}
public void show(Player player) {
List<Location> points = points(player);
if (points.isEmpty()) {
return;
}
showMarkerPreviews(player, points);
Particle.DustOptions dust = new Particle.DustOptions(Color.fromRGB(64, 210, 255), 1.4f);
Particle.DustOptions vertexDust = new Particle.DustOptions(Color.fromRGB(255, 220, 80), 1.8f);
for (Location point : points) {
spawn(player, point.getWorld(), point.getBlockX(), point.getBlockZ(), vertexDust, 8);
}
for (int i = 1; i < points.size(); i++) {
drawLine(player, points.get(i - 1), points.get(i), dust);
}
if (points.size() >= 3) {
drawLine(player, points.get(points.size() - 1), points.get(0), dust);
}
}
private void drawLine(Player player, Location a, Location b, Particle.DustOptions dust) {
if (!a.getWorld().equals(b.getWorld())) {
return;
}
double dx = b.getBlockX() - a.getBlockX();
double dz = b.getBlockZ() - a.getBlockZ();
int steps = Math.max(1, (int) Math.ceil(Math.max(Math.abs(dx), Math.abs(dz)) / 2.0D));
for (int i = 0; i <= steps; i++) {
double t = (double) i / (double) steps;
int x = (int) Math.round(a.getBlockX() + dx * t);
int z = (int) Math.round(a.getBlockZ() + dz * t);
spawn(player, a.getWorld(), x, z, dust, 1);
}
}
private void spawn(Player player, World world, int x, int z, Particle.DustOptions dust, int count) {
double y = world.getHighestBlockYAt(x, z) + 1.2D;
player.spawnParticle(Particle.DUST, x + 0.5D, y, z + 0.5D, count, 0.15D, 0.2D, 0.15D, 0.0D, dust);
}
private void showMarkerPreviews(Player player, List<Location> points) {
restoreMarkerPreviews(player);
List<Location> previews = new ArrayList<>();
for (Location point : points) {
World world = point.getWorld();
if (world == null) {
continue;
}
Location marker = markerLocation(world, point.getBlockX(), point.getBlockZ());
player.sendBlockChange(marker, Material.GOLD_BLOCK.createBlockData());
previews.add(marker);
}
markerPreviews.put(player.getUniqueId(), previews);
}
private void restoreMarkerPreviews(Player player) {
List<Location> previews = markerPreviews.remove(player.getUniqueId());
if (previews == null) {
return;
}
for (Location preview : previews) {
World world = preview.getWorld();
if (world == null) {
continue;
}
Block block = world.getBlockAt(preview);
player.sendBlockChange(preview, block.getBlockData());
}
}
private Location markerLocation(World world, int x, int z) {
int y = world.getHighestBlockYAt(x, z);
Block highest = world.getBlockAt(x, y, z);
if (highest.getType() == Material.GOLD_BLOCK) {
return highest.getLocation();
}
return world.getBlockAt(x, y + 1, z).getLocation();
}
}

View File

@@ -0,0 +1,225 @@
package com.librewiki.coalgov.service;
import com.librewiki.coalgov.model.Claim;
import com.librewiki.coalgov.model.ClaimPoint;
import com.librewiki.coalgov.storage.ClaimRepository;
import com.librewiki.coalgov.util.CoalMoney;
import com.librewiki.coalgov.util.Cuboid2D;
import org.bukkit.Location;
import org.bukkit.configuration.file.FileConfiguration;
import java.sql.SQLException;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public final class ClaimService {
private final ClaimRepository repository;
private final LandService landService;
private final EconomyService economyService;
private FileConfiguration config;
public ClaimService(ClaimRepository repository, LandService landService, EconomyService economyService, FileConfiguration config) {
this.repository = repository;
this.landService = landService;
this.economyService = economyService;
this.config = config;
}
public void setConfig(FileConfiguration config) {
this.config = config;
}
public BuyResult buy(UUID owner, Location center, String claimType, int radius) throws SQLException {
String type = claimType.toLowerCase();
if (!type.equals("homestead") && !type.equals("industrial")) {
return BuyResult.fail("Claim type must be homestead or industrial.");
}
int maxRadius = config.getInt("claims." + type + ".max_radius");
if (radius <= 0 || radius > maxRadius) {
return BuyResult.fail("Radius must be between 1 and " + maxRadius + ".");
}
Cuboid2D area = new Cuboid2D(
center.getWorld().getName(),
center.getBlockX() - radius,
center.getBlockZ() - radius,
center.getBlockX() + radius,
center.getBlockZ() + radius
);
if (repository.overlaps(area)) {
return BuyResult.fail("That claim overlaps an existing claim.");
}
if (landService.blocksPrivateClaim(area)) {
return BuyResult.fail("That land class cannot be claimed.");
}
long cost = adjustedCost(owner, type, area.area(), area);
if (cost > 0L && !economyService.charge(owner, cost, "claim_purchase")) {
return BuyResult.fail("You need " + CoalMoney.format(cost) + " for that claim.");
}
long id = repository.create(owner, area, type.toUpperCase(), null, cost, List.of());
return BuyResult.success(id, cost);
}
public BuyResult buyPolygon(UUID owner, String world, String claimType, List<ClaimPoint> vertices) throws SQLException {
String type = claimType.toLowerCase();
if (!type.equals("homestead") && !type.equals("industrial")) {
return BuyResult.fail("Claim type must be homestead or industrial.");
}
if (vertices.size() < 3) {
return BuyResult.fail("Use the claim wand to set at least 3 polygon points.");
}
Cuboid2D area = bounds(world, vertices);
int maxRadius = config.getInt("claims." + type + ".max_radius");
int maxWidth = maxRadius * 2 + 1;
if ((area.x2() - area.x1() + 1) > maxWidth || (area.z2() - area.z1() + 1) > maxWidth) {
return BuyResult.fail("Polygon bounds exceed max " + type + " size.");
}
if (repository.overlaps(area)) {
return BuyResult.fail("That claim overlaps an existing claim.");
}
if (landService.blocksPrivateClaim(area)) {
return BuyResult.fail("That land class cannot be claimed.");
}
long cost = adjustedCost(owner, type, polygonArea(vertices), area);
if (cost > 0L && !economyService.charge(owner, cost, "claim_purchase")) {
return BuyResult.fail("You need " + CoalMoney.format(cost) + " for that claim.");
}
long id = repository.create(owner, area, type.toUpperCase(), null, cost, vertices);
return BuyResult.success(id, cost);
}
public Optional<Claim> claimAt(Location location) throws SQLException {
return repository.findAt(location.getWorld().getName(), location.getBlockX(), location.getBlockZ());
}
public List<Claim> list(UUID owner) throws SQLException {
return repository.findByOwner(owner);
}
public List<Claim> listAll() throws SQLException {
return repository.listAll();
}
public Optional<Claim> findOwnedById(UUID owner, long id) throws SQLException {
return repository.findOwnedById(owner, id);
}
public SellResult sell(UUID owner, long id, long refund) throws SQLException {
Optional<Claim> claim = repository.findOwnedById(owner, id);
if (claim.isEmpty()) {
return SellResult.fail("No owned claim with that id.");
}
if (refund < 0L && !economyService.charge(owner, Math.abs(refund), "claim_sale_depletion")) {
return SellResult.fail("You need " + CoalMoney.format(Math.abs(refund)) + " to settle extracted land value.");
}
if (!repository.deleteOwned(owner, id)) {
if (refund < 0L) {
economyService.credit(owner, Math.abs(refund), "claim_sale_depletion_refund");
}
return SellResult.fail("Could not sell that claim.");
}
if (refund > 0L) {
economyService.credit(owner, refund, "claim_sale");
}
return SellResult.success(claim.get(), refund);
}
public boolean abandon(UUID owner, long id) throws SQLException {
return repository.deleteOwned(owner, id);
}
public TransferResult transfer(UUID owner, long id, UUID newOwner) throws SQLException {
if (owner.equals(newOwner)) {
return TransferResult.fail("You already own that claim.");
}
Optional<Claim> claim = repository.findOwnedById(owner, id);
if (claim.isEmpty()) {
return TransferResult.fail("No owned claim with that id.");
}
if (!repository.transferOwned(owner, id, newOwner)) {
return TransferResult.fail("Could not transfer that claim.");
}
return TransferResult.success(claim.get());
}
public long claimPurchaseCost(Claim claim) {
if (claim.purchaseCost() >= 0L) {
return claim.purchaseCost();
}
return assessedClaimCost(claim);
}
public long assessedClaimCost(Claim claim) {
return cost(claim.claimType().toLowerCase(), (long) (claim.x2() - claim.x1() + 1) * (claim.z2() - claim.z1() + 1));
}
private long cost(String type, long area) {
double base = config.getDouble("claims." + type + ".base_cost");
double perBlock = config.getDouble("claims." + type + ".cost_per_block");
return CoalMoney.fromCoalConfig(base + area * perBlock);
}
private long adjustedCost(UUID owner, String type, long areaBlocks, Cuboid2D bounds) throws SQLException {
long cost = cost(type, areaBlocks);
if (!type.equals("homestead") || !config.getBoolean("claims.homestead.first_claim.free", true)) {
return cost;
}
if (!list(owner).isEmpty()) {
return cost;
}
int freeRadius = config.getInt("claims.homestead.first_claim.max_radius", 12);
int maxWidth = freeRadius * 2 + 1;
if ((bounds.x2() - bounds.x1() + 1) > maxWidth || (bounds.z2() - bounds.z1() + 1) > maxWidth) {
return cost;
}
return 0L;
}
private Cuboid2D bounds(String world, List<ClaimPoint> vertices) {
int minX = vertices.stream().mapToInt(ClaimPoint::x).min().orElse(0);
int maxX = vertices.stream().mapToInt(ClaimPoint::x).max().orElse(0);
int minZ = vertices.stream().mapToInt(ClaimPoint::z).min().orElse(0);
int maxZ = vertices.stream().mapToInt(ClaimPoint::z).max().orElse(0);
return new Cuboid2D(world, minX, minZ, maxX, maxZ);
}
private long polygonArea(List<ClaimPoint> vertices) {
long sum = 0L;
for (int i = 0, j = vertices.size() - 1; i < vertices.size(); j = i++) {
ClaimPoint a = vertices.get(j);
ClaimPoint b = vertices.get(i);
sum += (long) a.x() * b.z() - (long) b.x() * a.z();
}
return Math.max(1L, Math.round(Math.abs(sum) / 2.0D));
}
public record BuyResult(boolean success, String message, long id, long cost) {
public static BuyResult fail(String message) {
return new BuyResult(false, message, -1L, 0L);
}
public static BuyResult success(long id, long cost) {
return new BuyResult(true, "", id, cost);
}
}
public record SellResult(boolean success, String message, Claim claim, long refund) {
public static SellResult fail(String message) {
return new SellResult(false, message, null, 0L);
}
public static SellResult success(Claim claim, long refund) {
return new SellResult(true, "", claim, refund);
}
}
public record TransferResult(boolean success, String message, Claim claim) {
public static TransferResult fail(String message) {
return new TransferResult(false, message, null);
}
public static TransferResult success(Claim claim) {
return new TransferResult(true, "", claim);
}
}
}

View File

@@ -0,0 +1,92 @@
package com.librewiki.coalgov.service;
import com.librewiki.coalgov.CoalGovPlugin;
import com.librewiki.coalgov.model.Claim;
import org.bukkit.Color;
import org.bukkit.Particle;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
public final class ClaimVisualService {
private static final int REPEATS = 12;
private static final int PERIOD_TICKS = 10;
private static final int STEP_BLOCKS = 2;
private final CoalGovPlugin plugin;
public ClaimVisualService(CoalGovPlugin plugin) {
this.plugin = plugin;
}
public void show(Player player, Claim claim) {
World world = plugin.getServer().getWorld(claim.world());
if (world == null) {
player.sendMessage(plugin.messages().text("&cThat claim's world is not loaded."));
return;
}
if (!player.getWorld().equals(world)) {
player.sendMessage(plugin.messages().text("&cThat claim is in world " + claim.world() + "."));
return;
}
Particle.DustOptions edgeDust = new Particle.DustOptions(Color.fromRGB(255, 196, 64), 1.4f);
Particle.DustOptions cornerDust = new Particle.DustOptions(Color.fromRGB(255, 80, 64), 1.8f);
new BukkitRunnable() {
private int remaining = REPEATS;
@Override
public void run() {
if (remaining-- <= 0 || !player.isOnline()) {
cancel();
return;
}
draw(player, world, claim, edgeDust, cornerDust);
}
}.runTaskTimer(plugin, 0L, PERIOD_TICKS);
}
private void draw(Player player, World world, Claim claim, Particle.DustOptions edgeDust, Particle.DustOptions cornerDust) {
if (claim.isPolygon()) {
for (int i = 0; i < claim.vertices().size(); i++) {
var a = claim.vertices().get(i);
var b = claim.vertices().get((i + 1) % claim.vertices().size());
drawLine(player, world, a.x(), a.z(), b.x(), b.z(), edgeDust);
spawnCorner(player, world, a.x(), a.z(), cornerDust);
}
return;
}
for (int x = claim.x1(); x <= claim.x2(); x += STEP_BLOCKS) {
spawnEdge(player, world, x, claim.z1(), edgeDust);
spawnEdge(player, world, x, claim.z2(), edgeDust);
}
for (int z = claim.z1(); z <= claim.z2(); z += STEP_BLOCKS) {
spawnEdge(player, world, claim.x1(), z, edgeDust);
spawnEdge(player, world, claim.x2(), z, edgeDust);
}
spawnCorner(player, world, claim.x1(), claim.z1(), cornerDust);
spawnCorner(player, world, claim.x1(), claim.z2(), cornerDust);
spawnCorner(player, world, claim.x2(), claim.z1(), cornerDust);
spawnCorner(player, world, claim.x2(), claim.z2(), cornerDust);
}
private void spawnEdge(Player player, World world, int x, int z, Particle.DustOptions dust) {
double y = world.getHighestBlockYAt(x, z) + 1.15;
player.spawnParticle(Particle.DUST, x + 0.5, y, z + 0.5, 1, 0.0, 0.15, 0.0, 0.0, dust);
}
private void drawLine(Player player, World world, int x1, int z1, int x2, int z2, Particle.DustOptions dust) {
double dx = x2 - x1;
double dz = z2 - z1;
int steps = Math.max(1, (int) Math.ceil(Math.max(Math.abs(dx), Math.abs(dz)) / STEP_BLOCKS));
for (int i = 0; i <= steps; i++) {
double t = (double) i / (double) steps;
spawnEdge(player, world, (int) Math.round(x1 + dx * t), (int) Math.round(z1 + dz * t), dust);
}
}
private void spawnCorner(Player player, World world, int x, int z, Particle.DustOptions dust) {
double y = world.getHighestBlockYAt(x, z) + 1.25;
player.spawnParticle(Particle.DUST, x + 0.5, y, z + 0.5, 8, 0.25, 0.35, 0.25, 0.0, dust);
}
}

View File

@@ -0,0 +1,176 @@
package com.librewiki.coalgov.service;
import com.librewiki.coalgov.CoalGovPlugin;
import org.bukkit.Color;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.Particle;
import org.bukkit.Sound;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.persistence.PersistentDataType;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
public final class DiviningRodService {
private static final List<Material> ORES = List.of(
Material.COAL_ORE,
Material.DEEPSLATE_COAL_ORE,
Material.COPPER_ORE,
Material.DEEPSLATE_COPPER_ORE,
Material.IRON_ORE,
Material.DEEPSLATE_IRON_ORE,
Material.GOLD_ORE,
Material.DEEPSLATE_GOLD_ORE,
Material.REDSTONE_ORE,
Material.DEEPSLATE_REDSTONE_ORE,
Material.LAPIS_ORE,
Material.DEEPSLATE_LAPIS_ORE,
Material.DIAMOND_ORE,
Material.DEEPSLATE_DIAMOND_ORE,
Material.EMERALD_ORE,
Material.DEEPSLATE_EMERALD_ORE,
Material.NETHER_GOLD_ORE,
Material.NETHER_QUARTZ_ORE,
Material.ANCIENT_DEBRIS
);
private final CoalGovPlugin plugin;
private final NamespacedKey rodKey;
private final Map<UUID, Long> lastUseMillis = new HashMap<>();
public DiviningRodService(CoalGovPlugin plugin) {
this.plugin = plugin;
this.rodKey = new NamespacedKey(plugin, "divining_rod");
}
public ItemStack rod() {
ItemStack item = new ItemStack(Material.BLAZE_ROD);
ItemMeta meta = item.getItemMeta();
meta.setDisplayName(plugin.messages().raw("&dCoalGov Divining Rod"));
meta.setLore(List.of(
plugin.messages().raw("&7Right-click to test the ground for ore."),
plugin.messages().raw("&7Issued by the Ministry Geological Survey.")
));
meta.getPersistentDataContainer().set(rodKey, PersistentDataType.BYTE, (byte) 1);
item.setItemMeta(meta);
return item;
}
public boolean isRod(ItemStack item) {
if (item == null || item.getType() != Material.BLAZE_ROD || !item.hasItemMeta()) {
return false;
}
return item.getItemMeta().getPersistentDataContainer().has(rodKey, PersistentDataType.BYTE);
}
public void scan(Player player) {
long remaining = cooldownRemainingMillis(player);
if (remaining > 0L) {
player.sendMessage(plugin.messages().text("&cThe rod is still settling. Try again in "
+ Math.ceil(remaining / 1000.0D) + "s."));
return;
}
lastUseMillis.put(player.getUniqueId(), System.currentTimeMillis());
ScanHit hit = nearestOre(player.getLocation());
if (hit == null) {
player.playSound(player.getLocation(), Sound.BLOCK_AMETHYST_BLOCK_CHIME, 0.35f, 0.55f);
player.spawnParticle(Particle.ASH, player.getLocation().add(0.0D, 1.0D, 0.0D), 10, 0.35D, 0.2D, 0.35D, 0.0D);
player.sendMessage(plugin.messages().text("&7The divining rod stays still. No ore signal nearby."));
return;
}
double strength = 1.0D - Math.min(1.0D, hit.distance() / Math.max(1.0D, scanDepth()));
float pitch = (float) (0.75D + strength);
int particles = 8 + (int) Math.round(strength * 18.0D);
player.playSound(player.getLocation(), Sound.BLOCK_AMETHYST_BLOCK_RESONATE, 0.75f, pitch);
player.spawnParticle(Particle.DUST, player.getLocation().add(0.0D, 1.0D, 0.0D), particles,
0.4D, 0.25D, 0.4D, 0.0D, new Particle.DustOptions(Color.fromRGB(190, 80, 255), 1.1f));
player.sendMessage(plugin.messages().text("&dThe divining rod pulls " + direction(hit)
+ " toward &f" + readable(hit.material()) + " &7(" + Math.round(hit.distance()) + " blocks, "
+ signalName(strength) + " signal)&d."));
}
private long cooldownRemainingMillis(Player player) {
long cooldownMillis = Math.max(0L, Math.round(plugin.getConfig()
.getDouble("divining_rod.cooldown_seconds", 5.0D) * 1000.0D));
long lastUse = lastUseMillis.getOrDefault(player.getUniqueId(), 0L);
return Math.max(0L, cooldownMillis - (System.currentTimeMillis() - lastUse));
}
private ScanHit nearestOre(Location origin) {
World world = origin.getWorld();
if (world == null) {
return null;
}
int radius = scanRadius();
int depth = scanDepth();
int originX = origin.getBlockX();
int originY = origin.getBlockY();
int originZ = origin.getBlockZ();
int minY = Math.max(world.getMinHeight(), originY - depth);
ScanHit nearest = null;
for (int x = originX - radius; x <= originX + radius; x++) {
for (int z = originZ - radius; z <= originZ + radius; z++) {
for (int y = originY; y >= minY; y--) {
Material material = world.getBlockAt(x, y, z).getType();
if (!ORES.contains(material)) {
continue;
}
double distance = origin.distance(new Location(world, x + 0.5D, y + 0.5D, z + 0.5D));
if (nearest == null || distance < nearest.distance()) {
nearest = new ScanHit(material, x - originX, y - originY, z - originZ, distance);
}
}
}
}
return nearest;
}
private int scanRadius() {
return Math.max(0, plugin.getConfig().getInt("divining_rod.scan_radius", 4));
}
private int scanDepth() {
return Math.max(1, plugin.getConfig().getInt("divining_rod.scan_depth", 48));
}
private String direction(ScanHit hit) {
if (hit.dx() == 0 && hit.dz() == 0) {
return "straight down";
}
String northSouth = hit.dz() < 0 ? "north" : hit.dz() > 0 ? "south" : "";
String eastWest = hit.dx() < 0 ? "west" : hit.dx() > 0 ? "east" : "";
if (northSouth.isEmpty()) {
return eastWest + " and down";
}
if (eastWest.isEmpty()) {
return northSouth + " and down";
}
return northSouth + "-" + eastWest + " and down";
}
private String signalName(double strength) {
if (strength >= 0.75D) {
return "strong";
}
if (strength >= 0.45D) {
return "steady";
}
return "faint";
}
private String readable(Material material) {
return material.name().toLowerCase().replace('_', ' ');
}
private record ScanHit(Material material, int dx, int dy, int dz, double distance) {
}
}

View File

@@ -0,0 +1,60 @@
package com.librewiki.coalgov.service;
import com.librewiki.coalgov.storage.EconomyRepository;
import org.bukkit.OfflinePlayer;
import java.sql.SQLException;
import java.util.UUID;
public final class EconomyService {
private final EconomyRepository repository;
private final long startingBalance;
public EconomyService(EconomyRepository repository, long startingBalance) {
this.repository = repository;
this.startingBalance = startingBalance;
}
public void ensurePlayer(OfflinePlayer player) throws SQLException {
repository.ensurePlayer(player, startingBalance);
}
public void ensureAccount(UUID uuid, String name) throws SQLException {
repository.ensureAccount(uuid, name, startingBalance);
}
public long balance(UUID uuid) throws SQLException {
return repository.balance(uuid);
}
public boolean deposit(UUID uuid, long amount) throws SQLException {
return repository.add(uuid, amount, null, uuid, "deposit");
}
public boolean withdraw(UUID uuid, long amount) throws SQLException {
return repository.subtract(uuid, amount, uuid, null, "withdrawal");
}
public boolean pay(UUID from, UUID to, long amount) throws SQLException {
return repository.transfer(from, to, amount, "payment");
}
public boolean grant(UUID uuid, long amount) throws SQLException {
return repository.add(uuid, amount, null, uuid, "admin_grant");
}
public boolean credit(UUID uuid, long amount, String reason) throws SQLException {
if (amount <= 0) {
return true;
}
return repository.add(uuid, amount, null, uuid, reason);
}
public boolean take(UUID uuid, long amount) throws SQLException {
return repository.subtract(uuid, amount, uuid, null, "admin_take");
}
public boolean charge(UUID uuid, long amount, String reason) throws SQLException {
return repository.subtract(uuid, amount, uuid, null, reason);
}
}

View File

@@ -0,0 +1,136 @@
package com.librewiki.coalgov.service;
import com.librewiki.coalgov.model.Fine;
import com.librewiki.coalgov.storage.GovernmentRepository;
import com.librewiki.coalgov.util.CoalMoney;
import org.bukkit.configuration.file.FileConfiguration;
import java.sql.SQLException;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public final class GovernmentService {
private final GovernmentRepository repository;
private final EconomyService economyService;
private FileConfiguration config;
public GovernmentService(GovernmentRepository repository, EconomyService economyService, FileConfiguration config) {
this.repository = repository;
this.economyService = economyService;
this.config = config;
}
public void setConfig(FileConfiguration config) {
this.config = config;
}
public long treasuryBalance() throws SQLException {
return repository.treasuryBalance();
}
public boolean grantFromTreasury(UUID target, long amount) throws SQLException {
if (amount <= 0L || !repository.spendTreasury(amount)) {
return false;
}
if (!economyService.credit(target, amount, "treasury_grant")) {
repository.addTreasury(amount);
return false;
}
return true;
}
public long marketSaleTax(UUID playerUuid, long gross) throws SQLException {
return transactionTax(playerUuid, gross, "government.taxes.market_sale_percent");
}
public long marketPurchaseTax(UUID playerUuid, long gross) throws SQLException {
return transactionTax(playerUuid, gross, "government.taxes.market_purchase_percent");
}
public void collectTax(long amount) throws SQLException {
repository.addTreasury(amount);
}
public boolean taxExempt(UUID uuid) throws SQLException {
return repository.taxExempt(uuid);
}
public void setTaxExempt(UUID uuid, boolean exempt) throws SQLException {
repository.setTaxExempt(uuid, exempt);
}
public long createFine(UUID playerUuid, UUID issuerUuid, long amount, String reason) throws SQLException {
if (amount <= 0L) {
return -1L;
}
return repository.createFine(playerUuid, issuerUuid, amount, reason);
}
public List<Fine> unpaidFines(UUID playerUuid) throws SQLException {
return repository.unpaidFines(playerUuid);
}
public PayFineResult payFine(UUID playerUuid, long id) throws SQLException {
Optional<Fine> fine = repository.findUnpaidFine(playerUuid, id);
if (fine.isEmpty()) {
return PayFineResult.fail("No unpaid fine with that id.");
}
return pay(playerUuid, fine.get());
}
public PayFineResult payAllFines(UUID playerUuid) throws SQLException {
List<Fine> fines = repository.unpaidFines(playerUuid);
if (fines.isEmpty()) {
return PayFineResult.fail("You have no unpaid fines.");
}
long total = fines.stream().mapToLong(Fine::amount).sum();
if (economyService.balance(playerUuid) < total) {
return PayFineResult.fail("You need " + CoalMoney.format(total) + " to pay all fines.");
}
long paid = 0L;
for (Fine fine : fines) {
PayFineResult result = pay(playerUuid, fine);
if (!result.success()) {
return paid > 0L
? PayFineResult.success(paid, "Paid " + CoalMoney.format(paid) + " before one fine failed.")
: result;
}
paid += result.amount();
}
return PayFineResult.success(paid, "");
}
private PayFineResult pay(UUID playerUuid, Fine fine) throws SQLException {
if (!economyService.charge(playerUuid, fine.amount(), "fine_payment")) {
return PayFineResult.fail("Balance too low.");
}
if (!repository.markFinePaid(fine.id())) {
economyService.credit(playerUuid, fine.amount(), "fine_payment_refund");
return PayFineResult.fail("Fine payment failed.");
}
repository.addTreasury(fine.amount());
return PayFineResult.success(fine.amount(), "");
}
private long transactionTax(UUID playerUuid, long gross, String configPath) throws SQLException {
if (gross <= 0L || repository.taxExempt(playerUuid)) {
return 0L;
}
double percent = Math.max(0.0D, config.getDouble(configPath, 0.0D));
if (percent <= 0.0D) {
return 0L;
}
return Math.min(gross, Math.max(0L, Math.round(gross * percent / 100.0D)));
}
public record PayFineResult(boolean success, String message, long amount) {
public static PayFineResult fail(String message) {
return new PayFineResult(false, message, 0L);
}
public static PayFineResult success(long amount, String message) {
return new PayFineResult(true, message, amount);
}
}
}

View File

@@ -0,0 +1,145 @@
package com.librewiki.coalgov.service;
import com.librewiki.coalgov.CoalGovPlugin;
import com.librewiki.coalgov.model.Claim;
import com.librewiki.coalgov.util.CoalMoney;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.BlockState;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.inventory.InventoryHolder;
import org.bukkit.inventory.ItemStack;
import java.util.LinkedHashMap;
import java.util.Map;
public final class LandAppraisalService {
private final CoalGovPlugin plugin;
private final ClaimService claimService;
public LandAppraisalService(CoalGovPlugin plugin, ClaimService claimService) {
this.plugin = plugin;
this.claimService = claimService;
}
public Appraisal appraise(Claim claim) {
World world = plugin.getServer().getWorld(claim.world());
if (world == null) {
long base = claimService.claimPurchaseCost(claim);
return new Appraisal(base, claimService.assessedClaimCost(claim), 0.0D, 0.0D, 0.0D, 0.0D, 0L, Map.of(), false);
}
Map<Material, Double> prices = resourcePrices();
Map<Material, Long> resources = new LinkedHashMap<>();
long solid = 0L;
long checked = 0L;
double resourceValue = 0.0D;
for (int x = claim.x1(); x <= claim.x2(); x++) {
for (int z = claim.z1(); z <= claim.z2(); z++) {
int top = world.getHighestBlockYAt(x, z);
for (int y = world.getMinHeight(); y <= top; y++) {
Block block = world.getBlockAt(x, y, z);
Material type = block.getType();
checked++;
if (!type.isAir()) {
solid++;
}
resourceValue += addResource(resources, prices, type, 1L);
resourceValue += inventoryResourceValue(block, resources, prices);
}
}
}
double intactRatio = checked == 0L ? 1.0D : (double) solid / (double) checked;
double minMultiplier = plugin.getConfig().getDouble("claims.resale.min_condition_multiplier", 0.25D);
minMultiplier = Math.max(0.0D, Math.min(1.0D, minMultiplier));
double conditionMultiplier = minMultiplier + ((1.0D - minMultiplier) * intactRatio);
long originalCost = claimService.claimPurchaseCost(claim);
long assessedCost = claimService.assessedClaimCost(claim);
double conditionValue = originalCost * conditionMultiplier;
double depletionCharge = originalCost == 0L ? assessedCost * (1.0D - conditionMultiplier) : 0.0D;
long total = Math.round(conditionValue + resourceValue - depletionCharge);
return new Appraisal(originalCost, assessedCost, conditionMultiplier, conditionValue, resourceValue, depletionCharge, total, resources, true);
}
private double inventoryResourceValue(Block block, Map<Material, Long> resources, Map<Material, Double> prices) {
BlockState state = block.getState();
if (!(state instanceof InventoryHolder holder)) {
return 0.0D;
}
double value = 0.0D;
for (ItemStack item : holder.getInventory().getContents()) {
if (item == null || item.getType().isAir()) {
continue;
}
value += addResource(resources, prices, item.getType(), item.getAmount());
}
return value;
}
private double addResource(Map<Material, Long> resources, Map<Material, Double> prices, Material material, long amount) {
Double price = prices.get(material);
if (price == null || amount <= 0L) {
return 0.0D;
}
resources.merge(material, amount, Long::sum);
return CoalMoney.fromCoal(price) * amount;
}
private Map<Material, Double> resourcePrices() {
Map<Material, Double> prices = defaultResourcePrices();
ConfigurationSection section = plugin.getConfig().getConfigurationSection("claims.resale.resource_values");
if (section == null) {
return prices;
}
for (String key : section.getKeys(false)) {
Material material = Material.matchMaterial(key);
if (material != null) {
prices.put(material, section.getDouble(key));
}
}
return prices;
}
private Map<Material, Double> defaultResourcePrices() {
Map<Material, Double> prices = new LinkedHashMap<>();
prices.put(Material.COBBLESTONE, 0.02D);
prices.put(Material.COAL, 1.0D);
prices.put(Material.COAL_BLOCK, 9.0D);
prices.put(Material.RAW_COPPER, 0.5D);
prices.put(Material.COPPER_INGOT, 1.0D);
prices.put(Material.RAW_IRON, 1.0D);
prices.put(Material.IRON_INGOT, 2.0D);
prices.put(Material.RAW_GOLD, 2.0D);
prices.put(Material.GOLD_INGOT, 4.0D);
prices.put(Material.DIAMOND, 16.0D);
prices.put(Material.EMERALD, 12.0D);
return prices;
}
public record Appraisal(
long originalCost,
long assessedCost,
double conditionMultiplier,
double conditionValue,
double rawResourceValue,
double depletionCharge,
long totalValue,
Map<Material, Long> resources,
boolean worldLoaded
) {
public long resourceValue() {
return Math.max(0L, Math.round(rawResourceValue));
}
public long depletionChargeRounded() {
return Math.max(0L, Math.round(depletionCharge));
}
public int conditionPercent() {
return (int) Math.round(conditionMultiplier * 100.0D);
}
}
}

View File

@@ -0,0 +1,69 @@
package com.librewiki.coalgov.service;
import com.librewiki.coalgov.model.LandClass;
import com.librewiki.coalgov.model.LandRegion;
import com.librewiki.coalgov.storage.LandRepository;
import com.librewiki.coalgov.util.Cuboid2D;
import org.bukkit.Location;
import org.bukkit.configuration.file.FileConfiguration;
import java.sql.SQLException;
import java.util.List;
import java.util.Optional;
public final class LandService {
private final LandRepository repository;
private FileConfiguration config;
public LandService(LandRepository repository, FileConfiguration config) {
this.repository = repository;
this.config = config;
}
public void setConfig(FileConfiguration config) {
this.config = config;
}
public Optional<LandRegion> regionAt(Location location) throws SQLException {
return repository.findAt(location.getWorld().getName(), location.getBlockX(), location.getBlockZ());
}
public LandClass classAt(Location location) throws SQLException {
return regionAt(location).map(LandRegion::landClass).orElse(defaultLandClass());
}
public Optional<LandRegion> findByName(String name) throws SQLException {
return repository.findByName(name);
}
public boolean create(String name, Cuboid2D area, LandClass landClass) throws SQLException {
return repository.create(name, area, landClass);
}
public boolean delete(String name) throws SQLException {
return repository.delete(name);
}
public List<LandRegion> listAll() throws SQLException {
return repository.listAll();
}
public boolean blocksPrivateClaim(Cuboid2D area) throws SQLException {
return repository.overlaps(
area,
LandClass.GOVERNMENT,
LandClass.PUBLIC,
LandClass.MINING_CONCESSION,
LandClass.PROTECTED_PRESERVE,
LandClass.BORDER_ZONE
);
}
private LandClass defaultLandClass() {
try {
return LandClass.valueOf(config.getString("land.default_land_class", "FREEHOLD").toUpperCase());
} catch (IllegalArgumentException exception) {
return LandClass.FREEHOLD;
}
}
}

View File

@@ -0,0 +1,336 @@
package com.librewiki.coalgov.service;
import com.librewiki.coalgov.CoalGovPlugin;
import com.librewiki.coalgov.storage.MarketRepository;
import com.librewiki.coalgov.util.CoalMoney;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import java.sql.SQLException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public final class MarketService {
public static final String TITLE = "Ministry Exchange";
private static final int SELL_ALL_SLOT = 4;
private final CoalGovPlugin plugin;
private final MarketRepository repository;
public MarketService(CoalGovPlugin plugin, MarketRepository repository) {
this.plugin = plugin;
this.repository = repository;
}
public void open(Player player) throws SQLException {
Inventory inventory = Bukkit.createInventory(null, 45, TITLE);
inventory.setItem(SELL_ALL_SLOT, button(Material.EMERALD, "&aSell appraised resources", List.of(
"&7Sells all priced resources in your inventory.",
"&7Price rises when Ministry stock is low."
)));
int slot = 9;
for (Material material : basePrices().keySet()) {
if (slot >= inventory.getSize()) {
break;
}
Quote quote = quote(material);
inventory.setItem(slot++, button(material, "&eBuy " + readable(material), List.of(
"&7Click: buy 1",
"&7Shift-click: buy 64",
"&7Stock: " + quote.stock(),
"&7Buy 1: " + CoalMoney.format(buyCost(quote, 1)),
"&7Buy 64: " + CoalMoney.format(buyCost(quote, 64)),
"&7Sell: " + CoalMoney.format(CoalMoney.fromCoal(quote.sellUnitPrice() * 64.0D)) + " per 64"
)));
}
player.openInventory(inventory);
}
public void sellAll(Player player) throws SQLException {
Map<Material, Double> prices = basePrices();
Map<Material, Long> simulatedStock = new LinkedHashMap<>();
Map<Material, Long> sold = new LinkedHashMap<>();
long grossCredit = 0L;
for (ItemStack item : player.getInventory().getStorageContents()) {
if (item == null || item.getType().isAir()) {
continue;
}
Material material = item.getType();
Double basePrice = prices.get(material);
if (basePrice == null) {
continue;
}
long stock = simulatedStock.containsKey(material)
? simulatedStock.get(material)
: repository.stock(material, initialStock(material));
grossCredit += sellCredit(material, basePrice, stock, item.getAmount());
simulatedStock.put(material, stock + item.getAmount());
sold.merge(material, (long) item.getAmount(), Long::sum);
}
if (grossCredit <= 0L) {
player.sendMessage(plugin.messages().text("&cNo appraised resources with enough value found in your inventory."));
return;
}
long tax = plugin.governmentService().marketSaleTax(player.getUniqueId(), grossCredit);
long credit = Math.max(0L, grossCredit - tax);
plugin.economyService().credit(player.getUniqueId(), credit, "market_sale");
plugin.governmentService().collectTax(tax);
removeSoldItems(player, sold);
for (var entry : sold.entrySet()) {
repository.addStock(entry.getKey(), entry.getValue());
}
if (tax > 0L) {
player.sendMessage(plugin.messages().text("&aSold resources for &f" + CoalMoney.format(credit) + " &aafter &f" + CoalMoney.format(tax) + " &atax."));
} else {
player.sendMessage(plugin.messages().text("&aSold resources for &f" + CoalMoney.format(credit) + "&a."));
}
}
public void buy(Player player, Material material, int amount) throws SQLException {
if (!basePrices().containsKey(material)) {
return;
}
if (amount <= 0) {
player.sendMessage(plugin.messages().text("&cInvalid purchase amount."));
return;
}
Quote quote = quote(material);
if (quote.stock() < amount) {
player.sendMessage(plugin.messages().text("&cThe Ministry has fewer than " + amount + " " + readable(material) + " in stock."));
return;
}
long cost = buyCost(quote, amount);
long tax = plugin.governmentService().marketPurchaseTax(player.getUniqueId(), cost);
long total = cost + tax;
if (plugin.economyService().balance(player.getUniqueId()) < total) {
player.sendMessage(plugin.messages().text("&cYou need " + CoalMoney.format(total) + "."));
return;
}
if (!hasRoomFor(player, material, amount)) {
player.sendMessage(plugin.messages().text("&cYou need more inventory space."));
return;
}
if (!plugin.economyService().charge(player.getUniqueId(), total, "market_purchase")) {
player.sendMessage(plugin.messages().text("&cPurchase failed."));
return;
}
if (!repository.removeStock(material, amount)) {
plugin.economyService().credit(player.getUniqueId(), total, "market_refund");
player.sendMessage(plugin.messages().text("&cThe Ministry ran out of stock."));
return;
}
if (!player.getInventory().addItem(new ItemStack(material, amount)).isEmpty()) {
repository.addStock(material, amount);
plugin.economyService().credit(player.getUniqueId(), total, "market_refund");
player.sendMessage(plugin.messages().text("&cYou need more inventory space."));
return;
}
plugin.governmentService().collectTax(tax);
if (tax > 0L) {
player.sendMessage(plugin.messages().text("&aBought " + amount + " " + readable(material) + " for &f" + CoalMoney.format(total) + "&a, including &f" + CoalMoney.format(tax) + " &atax."));
} else {
player.sendMessage(plugin.messages().text("&aBought " + amount + " " + readable(material) + " for &f" + CoalMoney.format(cost) + "&a."));
}
}
public boolean isSellAllSlot(int slot) {
return slot == SELL_ALL_SLOT;
}
public Map<Material, Double> basePrices() {
Map<Material, Double> prices = defaultPrices();
ConfigurationSection section = plugin.getConfig().getConfigurationSection("market.prices");
if (section == null) {
section = plugin.getConfig().getConfigurationSection("claims.resale.resource_values");
}
if (section == null) {
return prices;
}
for (String key : section.getKeys(false)) {
Material material = Material.matchMaterial(key);
if (material != null) {
prices.put(material, section.getDouble(key));
}
}
return prices;
}
private Quote quote(Material material) throws SQLException {
double basePrice = basePrices().get(material);
long stock = repository.stock(material, initialStock(material));
return quote(material, basePrice, stock);
}
private Quote quote(Material material, double basePrice, long stock) {
return new Quote(stock, sellUnitPrice(material, basePrice, stock), buyUnitPrice(material, basePrice, stock));
}
private long sellCredit(Material material, double basePrice, long startingStock, int amount) {
double total = 0.0D;
for (int index = 0; index < amount; index++) {
total += sellUnitPrice(material, basePrice, startingStock + index);
}
return Math.max(1L, CoalMoney.fromCoal(total));
}
private double sellUnitPrice(Material material, double basePrice, long stock) {
double target = Math.max(1.0D, targetStock());
double multiplier = target / Math.max(1.0D, stock);
multiplier = Math.max(minMultiplier(), Math.min(maxMultiplier(), multiplier));
return Math.max(coalEquivalentFloor(material), basePrice * multiplier * sellMultiplier());
}
private double buyUnitPrice(Material material, double basePrice, long stock) {
double sellPrice = sellUnitPrice(material, basePrice, stock);
double target = Math.max(1.0D, targetStock());
double multiplier = target / Math.max(1.0D, stock);
multiplier = Math.max(minMultiplier(), Math.min(maxMultiplier(), multiplier));
return Math.max(sellPrice, basePrice * multiplier * buyMultiplier());
}
private long buyCost(Quote quote, int amount) {
return Math.max(1L, CoalMoney.fromCoal(quote.buyUnitPrice() * amount));
}
private double coalEquivalentFloor(Material material) {
if (material == Material.COAL || material == Material.CHARCOAL) {
return 1.0D;
}
if (material == Material.COAL_BLOCK) {
return 9.0D;
}
String name = material.name();
if (name.endsWith("_LOG") || name.endsWith("_STEM")) {
return 1.0D;
}
if (name.endsWith("_PLANKS")) {
return 1.5D / 8.0D;
}
if (material == Material.STICK) {
return 0.5D / 8.0D;
}
return 0.0D;
}
private boolean hasRoomFor(Player player, Material material, int amount) {
int remaining = amount;
for (ItemStack item : player.getInventory().getStorageContents()) {
if (item == null || item.getType().isAir()) {
remaining -= material.getMaxStackSize();
} else if (item.getType() == material) {
remaining -= Math.max(0, item.getMaxStackSize() - item.getAmount());
}
if (remaining <= 0) {
return true;
}
}
return false;
}
private void removeSoldItems(Player player, Map<Material, Long> sold) {
Map<Material, Long> remainingByMaterial = new LinkedHashMap<>(sold);
for (ItemStack item : player.getInventory().getStorageContents()) {
if (item == null || item.getType().isAir()) {
continue;
}
Long remaining = remainingByMaterial.get(item.getType());
if (remaining == null || remaining <= 0L) {
continue;
}
if (remaining >= item.getAmount()) {
remainingByMaterial.put(item.getType(), remaining - item.getAmount());
item.setAmount(0);
} else {
item.setAmount((int) (item.getAmount() - remaining));
remainingByMaterial.put(item.getType(), 0L);
}
}
}
private long initialStock() {
return plugin.getConfig().getLong("market.dynamic.initial_stock", 1024L);
}
private long initialStock(Material material) {
return plugin.getConfig().getLong("market.initial_stock." + material.name(), initialStock());
}
private long targetStock() {
return plugin.getConfig().getLong("market.dynamic.target_stock", 1024L);
}
private double minMultiplier() {
return plugin.getConfig().getDouble("market.dynamic.min_multiplier", 0.35D);
}
private double maxMultiplier() {
return plugin.getConfig().getDouble("market.dynamic.max_multiplier", 3.0D);
}
private double sellMultiplier() {
return plugin.getConfig().getDouble("market.dynamic.sell_multiplier", 0.85D);
}
private double buyMultiplier() {
return plugin.getConfig().getDouble("market.dynamic.buy_multiplier", 1.15D);
}
private ItemStack button(Material material, String name, List<String> lore) {
ItemStack item = new ItemStack(material);
ItemMeta meta = item.getItemMeta();
meta.setDisplayName(plugin.messages().raw(name));
meta.setLore(lore.stream().map(plugin.messages()::raw).toList());
item.setItemMeta(meta);
return item;
}
private Map<Material, Double> defaultPrices() {
Map<Material, Double> prices = new LinkedHashMap<>();
prices.put(Material.DIRT, 0.01D);
prices.put(Material.COBBLESTONE, 0.02D);
prices.put(Material.SAND, 0.05D);
prices.put(Material.GRAVEL, 0.04D);
prices.put(Material.OAK_LOG, 0.25D);
prices.put(Material.SPRUCE_LOG, 0.25D);
prices.put(Material.BIRCH_LOG, 0.25D);
prices.put(Material.TORCH, 0.10D);
prices.put(Material.COAL, 1.0D);
prices.put(Material.COAL_BLOCK, 9.0D);
prices.put(Material.RAW_COPPER, 0.5D);
prices.put(Material.IRON_INGOT, 2.0D);
prices.put(Material.GOLD_INGOT, 4.0D);
prices.put(Material.DIAMOND, 16.0D);
prices.put(Material.EMERALD, 12.0D);
prices.put(Material.WHEAT, 0.35D);
prices.put(Material.BREAD, 1.25D);
prices.put(Material.APPLE, 0.75D);
prices.put(Material.CARROT, 0.25D);
prices.put(Material.POTATO, 0.20D);
prices.put(Material.BAKED_POTATO, 0.45D);
prices.put(Material.BEETROOT, 0.20D);
prices.put(Material.MELON_SLICE, 0.15D);
prices.put(Material.SWEET_BERRIES, 0.20D);
prices.put(Material.GLOW_BERRIES, 0.35D);
prices.put(Material.COOKIE, 0.25D);
prices.put(Material.PUMPKIN_PIE, 1.0D);
prices.put(Material.COOKED_CHICKEN, 1.25D);
prices.put(Material.COOKED_PORKCHOP, 1.5D);
prices.put(Material.COOKED_BEEF, 1.75D);
prices.put(Material.COOKED_MUTTON, 1.5D);
prices.put(Material.COOKED_RABBIT, 1.25D);
prices.put(Material.COOKED_COD, 1.0D);
return prices;
}
private String readable(Material material) {
return material.name().toLowerCase().replace('_', ' ');
}
private record Quote(long stock, double sellUnitPrice, double buyUnitPrice) {
}
}

View File

@@ -0,0 +1,41 @@
package com.librewiki.coalgov.service;
import com.librewiki.coalgov.CoalGovPlugin;
import com.librewiki.coalgov.model.Claim;
import com.librewiki.coalgov.model.LandClass;
import com.librewiki.coalgov.model.LandRegion;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import java.sql.SQLException;
import java.util.Optional;
public final class MiningService {
private final CoalGovPlugin plugin;
private final LandService landService;
private final ClaimService claimService;
private final PermitService permitService;
public MiningService(CoalGovPlugin plugin, LandService landService, ClaimService claimService, PermitService permitService) {
this.plugin = plugin;
this.landService = landService;
this.claimService = claimService;
this.permitService = permitService;
}
public boolean canMineCoal(Player player, Location location) throws SQLException {
if (plugin.hasAdminBypass(player)) {
return true;
}
Optional<LandRegion> region = landService.regionAt(location);
LandClass landClass = region.map(LandRegion::landClass).orElse(landService.classAt(location));
if (landClass == LandClass.FREEHOLD) {
Optional<Claim> claim = claimService.claimAt(location);
return claim.isPresent() && claim.get().ownerUuid().equals(player.getUniqueId());
}
if (landClass == LandClass.MINING_CONCESSION) {
return region.isPresent() && permitService.hasMiningPermit(player.getUniqueId(), region.get().name());
}
return false;
}
}

View File

@@ -0,0 +1,218 @@
package com.librewiki.coalgov.service;
import com.librewiki.coalgov.CoalGovPlugin;
import com.librewiki.coalgov.model.ClaimPoint;
import com.librewiki.coalgov.model.CoalNpc;
import com.librewiki.coalgov.model.NpcZone;
import com.librewiki.coalgov.storage.NpcRepository;
import com.librewiki.coalgov.util.CoalMoney;
import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.api.npc.NPC;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import java.nio.charset.StandardCharsets;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Random;
import java.util.UUID;
public final class NpcService {
private final CoalGovPlugin plugin;
private final NpcRepository repository;
private final Random random = new Random();
public NpcService(CoalGovPlugin plugin, NpcRepository repository) {
this.plugin = plugin;
this.repository = repository;
}
public long createZone(String name, String world, List<ClaimPoint> points) throws SQLException {
return repository.createZone(name, world, points);
}
public long createNpc(String type, String name, NpcZone zone, Location location) throws SQLException {
NPC npc = CitizensAPI.getNPCRegistry().createNPC(EntityType.VILLAGER, name);
npc.spawn(location);
npc.setProtected(true);
UUID accountUuid = UUID.nameUUIDFromBytes(("coalgov:npc:" + npc.getId()).getBytes(StandardCharsets.UTF_8));
plugin.economyService().ensureAccount(accountUuid, "NPC-" + name);
return repository.createNpc(npc.getId(), accountUuid, type, name, zone.id(),
location.getWorld().getName(), location.getX(), location.getY(), location.getZ());
}
public boolean removeNpc(long id) throws SQLException {
Optional<CoalNpc> coalNpc = repository.findNpc(id);
if (coalNpc.isPresent()) {
NPC npc = CitizensAPI.getNPCRegistry().getById(coalNpc.get().citizensId());
if (npc != null) {
npc.destroy();
}
}
return repository.deleteNpc(id);
}
public Optional<CoalNpc> findNpc(long id) throws SQLException {
return repository.findNpc(id);
}
public Optional<CoalNpc> findByCitizensId(int citizensId) throws SQLException {
return repository.findByCitizensId(citizensId);
}
public Optional<NpcZone> findZone(String name) throws SQLException {
return repository.findZone(name);
}
public List<CoalNpc> listNpcs() throws SQLException {
return repository.listNpcs();
}
public List<NpcZone> listZones() throws SQLException {
return repository.listZones();
}
public Map<Material, Long> inventory(long npcId) throws SQLException {
return repository.inventory(npcId);
}
public void addInventory(long npcId, Material material, long amount) throws SQLException {
repository.addInventory(npcId, material, amount);
}
public long balance(CoalNpc npc) throws SQLException {
return plugin.economyService().balance(npc.accountUuid());
}
public HaggleQuote quote(CoalNpc npc, String mode, Material material, int amount, long offer) throws SQLException {
double base = plugin.marketService().basePrices().getOrDefault(material, 1.0D);
long fair = Math.max(1L, CoalMoney.fromCoal(base * amount));
int maxDiscount = plugin.getConfig().getInt("npc.trader.max_discount_percent", 15);
int maxSurcharge = plugin.getConfig().getInt("npc.trader.max_surcharge_percent", 20);
if (mode.equalsIgnoreCase("buy")) {
long stock = repository.inventoryAmount(npc.id(), material);
long listPrice = Math.max(1L, Math.round(fair * 1.10D));
long minimum = Math.max(1L, Math.round(listPrice * (100.0D - maxDiscount) / 100.0D));
long counter = Math.max(minimum, Math.min(listPrice, offer <= 0L ? listPrice : Math.round((offer + listPrice) / 2.0D)));
boolean possible = stock >= amount;
boolean accepted = possible && offer >= minimum;
return new HaggleQuote(mode.toLowerCase(), material, amount, offer, minimum, listPrice, counter, accepted, possible ? "" : "I do not have enough stock.");
}
long maximum = Math.max(1L, Math.round(fair * (100.0D + maxSurcharge) / 100.0D));
long target = Math.max(1L, Math.round(fair * 0.90D));
long counter = Math.min(maximum, Math.max(target, offer <= 0L ? target : Math.round((offer + target) / 2.0D)));
boolean possible = balance(npc) >= offer;
boolean accepted = possible && offer <= maximum;
return new HaggleQuote(mode.toLowerCase(), material, amount, offer, target, maximum, counter, accepted, possible ? "" : "I do not have enough coal.");
}
public boolean completeTrade(Player player, CoalNpc npc, HaggleQuote quote, long finalPrice) throws SQLException {
if (quote.mode().equals("buy")) {
if (repository.inventoryAmount(npc.id(), quote.material()) < quote.amount()) {
return false;
}
if (!plugin.economyService().charge(player.getUniqueId(), finalPrice, "npc_purchase")) {
return false;
}
repository.removeInventory(npc.id(), quote.material(), quote.amount());
plugin.economyService().credit(npc.accountUuid(), finalPrice, "npc_sale");
player.getInventory().addItem(new ItemStack(quote.material(), quote.amount()));
return true;
}
if (!playerHas(player, quote.material(), quote.amount()) || balance(npc) < finalPrice) {
return false;
}
if (!plugin.economyService().charge(npc.accountUuid(), finalPrice, "npc_purchase")) {
return false;
}
removePlayerItems(player, quote.material(), quote.amount());
plugin.economyService().credit(player.getUniqueId(), finalPrice, "npc_sale");
repository.addInventory(npc.id(), quote.material(), quote.amount());
return true;
}
public void tickWorkers() {
try {
for (CoalNpc coalNpc : repository.listNpcs()) {
if (!coalNpc.worker()) {
continue;
}
NPC npc = CitizensAPI.getNPCRegistry().getById(coalNpc.citizensId());
Optional<NpcZone> zone = repository.findZone(coalNpc.zoneId());
if (npc == null || zone.isEmpty()) {
continue;
}
if (!npc.isSpawned()) {
World world = Bukkit.getWorld(coalNpc.world());
if (world != null) {
npc.spawn(new Location(world, coalNpc.x(), coalNpc.y(), coalNpc.z()));
}
}
if (npc.isSpawned() && !npc.getNavigator().isNavigating()) {
Location target = randomPoint(zone.get());
if (target != null) {
npc.getNavigator().setTarget(target);
}
}
Material material = Material.matchMaterial(plugin.getConfig().getString("npc.worker.produce_material", "BREAD"));
int amount = plugin.getConfig().getInt("npc.worker.produce_amount", 2);
if (material != null && amount > 0) {
repository.addInventory(coalNpc.id(), material, amount);
}
}
} catch (SQLException exception) {
plugin.getLogger().warning("NPC worker tick failed: " + exception.getMessage());
}
}
private Location randomPoint(NpcZone zone) {
World world = Bukkit.getWorld(zone.world());
if (world == null) {
return null;
}
for (int tries = 0; tries < 32; tries++) {
int x = zone.x1() + random.nextInt(Math.max(1, zone.x2() - zone.x1() + 1));
int z = zone.z1() + random.nextInt(Math.max(1, zone.z2() - zone.z1() + 1));
if (zone.contains(world.getName(), x, z)) {
return new Location(world, x + 0.5D, world.getHighestBlockYAt(x, z), z + 0.5D);
}
}
return new Location(world, zone.x1() + 0.5D, world.getHighestBlockYAt(zone.x1(), zone.z1()), zone.z1() + 0.5D);
}
private boolean playerHas(Player player, Material material, int amount) {
int found = 0;
for (ItemStack item : player.getInventory().getStorageContents()) {
if (item != null && item.getType() == material) {
found += item.getAmount();
}
}
return found >= amount;
}
private void removePlayerItems(Player player, Material material, int amount) {
int remaining = amount;
for (ItemStack item : player.getInventory().getStorageContents()) {
if (item == null || item.getType() != material) {
continue;
}
int take = Math.min(remaining, item.getAmount());
item.setAmount(item.getAmount() - take);
remaining -= take;
if (remaining <= 0) {
return;
}
}
}
public record HaggleQuote(String mode, Material material, int amount, long offer, long floor, long ceiling,
long counter, boolean accepted, String reason) {
}
}

View File

@@ -0,0 +1,104 @@
package com.librewiki.coalgov.service;
import com.librewiki.coalgov.CoalGovPlugin;
import com.librewiki.coalgov.util.CoalMoney;
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.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class OpenRouterService {
private static final Pattern MESSAGE_PATTERN = Pattern.compile("\"message\"\\s*:\\s*\"((?:\\\\.|[^\"])*)\"");
private static final Pattern COUNTER_PATTERN = Pattern.compile("\"counter\"\\s*:\\s*(\\d+)");
private static final Pattern ACCEPT_PATTERN = Pattern.compile("\"accept\"\\s*:\\s*(true|false)", Pattern.CASE_INSENSITIVE);
private final CoalGovPlugin plugin;
private final HttpClient client = HttpClient.newHttpClient();
public OpenRouterService(CoalGovPlugin plugin) {
this.plugin = plugin;
}
public CompletableFuture<Optional<HaggleAiResult>> haggle(String prompt, long minCounter, long maxCounter) {
if (!plugin.getConfig().getBoolean("npc.ai.enabled", true)) {
return CompletableFuture.completedFuture(Optional.empty());
}
String key = plugin.getConfig().getString("npc.ai.api_key", "");
if (key == null || key.isBlank()) {
return CompletableFuture.completedFuture(Optional.empty());
}
String model = plugin.getConfig().getString("npc.ai.model", "openai/gpt-4.1-mini");
int timeoutMs = plugin.getConfig().getInt("npc.ai.timeout_ms", 4000);
String body = """
{"model":"%s","messages":[{"role":"system","content":"You are a Minecraft CoalGov NPC trader. Return only compact JSON with accept boolean, counter integer, and message string. The counter must stay within the provided bounds."},{"role":"user","content":"%s"}],"temperature":0.8}
""".formatted(escape(model), escape(prompt));
HttpRequest request = HttpRequest.newBuilder(URI.create("https://openrouter.ai/api/v1/chat/completions"))
.timeout(Duration.ofMillis(timeoutMs))
.header("Authorization", "Bearer " + key)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
return client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(response -> parse(response.body(), minCounter, maxCounter))
.exceptionally(exception -> Optional.empty());
}
private Optional<HaggleAiResult> parse(String response, long minCounter, long maxCounter) {
String content = extractContent(response);
Matcher counterMatcher = COUNTER_PATTERN.matcher(content);
Matcher acceptMatcher = ACCEPT_PATTERN.matcher(content);
Matcher messageMatcher = MESSAGE_PATTERN.matcher(content);
if (!counterMatcher.find()) {
return Optional.empty();
}
long counter = Long.parseLong(counterMatcher.group(1));
counter = Math.max(minCounter, Math.min(maxCounter, counter));
boolean accept = acceptMatcher.find() && Boolean.parseBoolean(acceptMatcher.group(1));
String message = messageMatcher.find() ? unescape(messageMatcher.group(1)) : "I can work with " + CoalMoney.format(counter) + ".";
return Optional.of(new HaggleAiResult(accept, counter, message));
}
private String extractContent(String response) {
int index = response.indexOf("\"content\"");
if (index < 0) {
return response;
}
int start = response.indexOf('"', index + 9);
if (start < 0) {
return response;
}
start++;
StringBuilder builder = new StringBuilder();
boolean escaped = false;
for (int i = start; i < response.length(); i++) {
char c = response.charAt(i);
if (escaped) {
builder.append(c);
escaped = false;
} else if (c == '\\') {
escaped = true;
} else if (c == '"') {
break;
} else {
builder.append(c);
}
}
return builder.toString();
}
private String escape(String value) {
return value.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n");
}
private String unescape(String value) {
return value.replace("\\\"", "\"").replace("\\n", " ").replace("\\\\", "\\");
}
public record HaggleAiResult(boolean accept, long counter, String message) {
}
}

View File

@@ -0,0 +1,67 @@
package com.librewiki.coalgov.service;
import com.librewiki.coalgov.model.LandClass;
import com.librewiki.coalgov.model.LandRegion;
import com.librewiki.coalgov.model.Permit;
import com.librewiki.coalgov.model.PermitType;
import com.librewiki.coalgov.storage.PermitRepository;
import com.librewiki.coalgov.util.CoalMoney;
import org.bukkit.configuration.file.FileConfiguration;
import java.sql.SQLException;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public final class PermitService {
private final PermitRepository repository;
private final LandService landService;
private final EconomyService economyService;
private FileConfiguration config;
public PermitService(PermitRepository repository, LandService landService, EconomyService economyService, FileConfiguration config) {
this.repository = repository;
this.landService = landService;
this.economyService = economyService;
this.config = config;
}
public void setConfig(FileConfiguration config) {
this.config = config;
}
public BuyResult buyMining(UUID owner, String regionName) throws SQLException {
Optional<LandRegion> region = landService.findByName(regionName);
if (region.isEmpty()) {
return BuyResult.fail("No land region named " + regionName + ".");
}
if (region.get().landClass() != LandClass.MINING_CONCESSION) {
return BuyResult.fail("That region is not a mining concession.");
}
long cost = CoalMoney.fromCoalConfig(config.getDouble("permits.mining.cost", 64.0D));
if (!economyService.charge(owner, cost, "permit_purchase")) {
return BuyResult.fail("You need " + CoalMoney.format(cost) + " for that permit.");
}
long expiresAt = System.currentTimeMillis() + config.getLong("permits.mining.duration_hours", 72L) * 60L * 60L * 1000L;
long id = repository.create(owner, PermitType.MINING, region.get().name(), expiresAt);
return BuyResult.success(id, cost, expiresAt);
}
public boolean hasMiningPermit(UUID owner, String regionName) throws SQLException {
return repository.hasActive(owner, PermitType.MINING, regionName, System.currentTimeMillis());
}
public List<Permit> listActive(UUID owner) throws SQLException {
return repository.listActive(owner, System.currentTimeMillis());
}
public record BuyResult(boolean success, String message, long id, long cost, long expiresAt) {
public static BuyResult fail(String message) {
return new BuyResult(false, message, -1L, 0L, 0L);
}
public static BuyResult success(long id, long cost, long expiresAt) {
return new BuyResult(true, "", id, cost, expiresAt);
}
}
}

View File

@@ -0,0 +1,97 @@
package com.librewiki.coalgov.service;
import com.librewiki.coalgov.CoalGovPlugin;
import com.librewiki.coalgov.model.LandClass;
import com.librewiki.coalgov.util.Cuboid2D;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.Sign;
import org.bukkit.block.data.Rotatable;
import org.bukkit.block.sign.Side;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.Player;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
public final class SpawnGuideService {
private final CoalGovPlugin plugin;
public SpawnGuideService(CoalGovPlugin plugin) {
this.plugin = plugin;
}
public void setup(Player player) throws SQLException {
Location center = player.getLocation().toBlockLocation();
World world = player.getWorld();
world.setSpawnLocation(center);
if (plugin.getConfig().getBoolean("spawn.guide.create_government_region", true)) {
createSpawnRegion(center);
}
placeGuide(center);
}
private void createSpawnRegion(Location center) throws SQLException {
int radius = plugin.getConfig().getInt("spawn.protection_radius", 32);
Cuboid2D area = new Cuboid2D(
center.getWorld().getName(),
center.getBlockX() - radius,
center.getBlockZ() - radius,
center.getBlockX() + radius,
center.getBlockZ() + radius
);
plugin.landService().delete("spawn");
plugin.landService().create("spawn", area, LandClass.GOVERNMENT);
}
private void placeGuide(Location center) {
List<String> lines = guideLines();
int signs = Math.max(1, (int) Math.ceil(lines.size() / 4.0D));
int startX = center.getBlockX() - signs + 1;
int z = center.getBlockZ() + 3;
for (int i = 0; i < signs; i++) {
int x = startX + i * 2;
int y = center.getWorld().getHighestBlockYAt(x, z);
Block base = center.getWorld().getBlockAt(x, y, z);
base.setType(Material.POLISHED_ANDESITE);
Block signBlock = center.getWorld().getBlockAt(x, y + 1, z);
signBlock.setType(Material.OAK_SIGN);
if (signBlock.getBlockData() instanceof Rotatable rotatable) {
rotatable.setRotation(org.bukkit.block.BlockFace.SOUTH);
signBlock.setBlockData(rotatable);
}
if (signBlock.getState() instanceof Sign sign) {
for (int line = 0; line < 4; line++) {
int index = i * 4 + line;
sign.getSide(Side.FRONT).setLine(line, index < lines.size() ? lines.get(index) : "");
}
sign.update(true, false);
}
}
}
private List<String> guideLines() {
ConfigurationSection section = plugin.getConfig().getConfigurationSection("spawn.guide");
List<String> lines = section == null ? List.of() : section.getStringList("lines");
if (!lines.isEmpty()) {
return lines;
}
List<String> defaults = new ArrayList<>();
defaults.add("COALGOV BASIN");
defaults.add("Free first");
defaults.add("homestead:");
defaults.add("/claim buy");
defaults.add("Use /claim wand");
defaults.add("for polygons");
defaults.add("Coal seams:");
defaults.add("redwood ridges");
defaults.add("Permits:");
defaults.add("/permit buy");
defaults.add("mining <region>");
defaults.add("/market trades");
return defaults;
}
}

View File

@@ -0,0 +1,83 @@
package com.librewiki.coalgov.service;
import com.librewiki.coalgov.CoalGovPlugin;
import com.librewiki.coalgov.storage.SuperFurnaceRepository;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.block.Block;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.ShapedRecipe;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.persistence.PersistentDataType;
import java.sql.SQLException;
import java.util.List;
public final class SuperFurnaceService {
private static final int SPEED_MULTIPLIER = 5;
private static final int FUEL_MULTIPLIER = 4;
private final CoalGovPlugin plugin;
private final SuperFurnaceRepository repository;
private final NamespacedKey itemKey;
private final NamespacedKey recipeKey;
public SuperFurnaceService(CoalGovPlugin plugin, SuperFurnaceRepository repository) {
this.plugin = plugin;
this.repository = repository;
this.itemKey = new NamespacedKey(plugin, "super_furnace");
this.recipeKey = new NamespacedKey(plugin, "super_furnace_recipe");
}
public void registerRecipe() {
Bukkit.removeRecipe(recipeKey);
ShapedRecipe recipe = new ShapedRecipe(recipeKey, item());
recipe.shape("SSS", "SFS", "SSS");
recipe.setIngredient('S', Material.STONE);
recipe.setIngredient('F', Material.FURNACE);
Bukkit.addRecipe(recipe);
}
public ItemStack item() {
ItemStack item = new ItemStack(Material.FURNACE);
ItemMeta meta = item.getItemMeta();
meta.setDisplayName(plugin.messages().raw("&6CoalGov Super Furnace"));
meta.setLore(List.of(
plugin.messages().raw("&75x smelting speed."),
plugin.messages().raw("&7Burns fuel 4x faster."),
plugin.messages().raw("&7Crafted with stone around a furnace.")
));
meta.getPersistentDataContainer().set(itemKey, PersistentDataType.BYTE, (byte) 1);
item.setItemMeta(meta);
return item;
}
public boolean isItem(ItemStack item) {
if (item == null || item.getType() != Material.FURNACE || !item.hasItemMeta()) {
return false;
}
return item.getItemMeta().getPersistentDataContainer().has(itemKey, PersistentDataType.BYTE);
}
public void add(Block block) throws SQLException {
repository.add(block.getLocation());
}
public void remove(Block block) throws SQLException {
repository.remove(block.getLocation());
}
public boolean isSuperFurnace(Block block) throws SQLException {
return block.getType() == Material.FURNACE && repository.exists(block.getLocation());
}
public int fasterCookTime(int cookTime) {
return Math.max(1, (int) Math.ceil(cookTime / (double) SPEED_MULTIPLIER));
}
public int shorterBurnTime(int burnTime) {
return Math.max(1, (int) Math.floor(burnTime / (double) FUEL_MULTIPLIER));
}
}

View File

@@ -0,0 +1,7 @@
package com.librewiki.coalgov.service;
public final class TaxService {
public void runMaintenance() {
// Placeholder for future lease expiration and recurring tax collection.
}
}

View File

@@ -0,0 +1,227 @@
package com.librewiki.coalgov.storage;
import com.librewiki.coalgov.model.Claim;
import com.librewiki.coalgov.model.ClaimPoint;
import com.librewiki.coalgov.util.Cuboid2D;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public final class ClaimRepository {
private final Database database;
public ClaimRepository(Database database) {
this.database = database;
}
public long create(UUID owner, Cuboid2D area, String claimType, Long expiresAt) throws SQLException {
return create(owner, area, claimType, expiresAt, -1L, List.of());
}
public long create(UUID owner, Cuboid2D area, String claimType, Long expiresAt, long purchaseCost, List<ClaimPoint> vertices) throws SQLException {
boolean originalAutoCommit = database.connection().getAutoCommit();
database.connection().setAutoCommit(false);
try (PreparedStatement insert = database.connection().prepareStatement("""
INSERT INTO claims(owner_uuid, world, x1, z1, x2, z2, claim_type, expires_at, tax_due, purchase_cost, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?)
""", Statement.RETURN_GENERATED_KEYS)) {
insert.setString(1, owner.toString());
insert.setString(2, area.world());
insert.setInt(3, area.x1());
insert.setInt(4, area.z1());
insert.setInt(5, area.x2());
insert.setInt(6, area.z2());
insert.setString(7, claimType);
if (expiresAt == null) {
insert.setObject(8, null);
} else {
insert.setLong(8, expiresAt);
}
insert.setLong(9, purchaseCost);
insert.setLong(10, System.currentTimeMillis());
insert.executeUpdate();
try (ResultSet keys = insert.getGeneratedKeys()) {
long id = keys.next() ? keys.getLong(1) : -1L;
insertVertices(id, vertices);
database.connection().commit();
return id;
}
} catch (SQLException exception) {
database.connection().rollback();
throw exception;
} finally {
database.connection().setAutoCommit(originalAutoCommit);
}
}
public boolean deleteOwned(UUID owner, long id) throws SQLException {
boolean originalAutoCommit = database.connection().getAutoCommit();
database.connection().setAutoCommit(false);
try {
try (PreparedStatement deleteVertices = database.connection().prepareStatement(
"DELETE FROM claim_vertices WHERE claim_id = ?")) {
deleteVertices.setLong(1, id);
deleteVertices.executeUpdate();
}
try (PreparedStatement delete = database.connection().prepareStatement(
"DELETE FROM claims WHERE id = ? AND owner_uuid = ?")) {
delete.setLong(1, id);
delete.setString(2, owner.toString());
boolean deleted = delete.executeUpdate() == 1;
database.connection().commit();
return deleted;
}
} catch (SQLException exception) {
database.connection().rollback();
throw exception;
} finally {
database.connection().setAutoCommit(originalAutoCommit);
}
}
public Optional<Claim> findAt(String world, int x, int z) throws SQLException {
try (PreparedStatement statement = database.connection().prepareStatement("""
SELECT * FROM claims
WHERE world = ? AND x1 <= ? AND x2 >= ? AND z1 <= ? AND z2 >= ?
ORDER BY id DESC
""")) {
statement.setString(1, world);
statement.setInt(2, x);
statement.setInt(3, x);
statement.setInt(4, z);
statement.setInt(5, z);
try (ResultSet result = statement.executeQuery()) {
while (result.next()) {
Claim claim = map(result);
if (claim.contains(world, x, z)) {
return Optional.of(claim);
}
}
return Optional.empty();
}
}
}
public List<Claim> findByOwner(UUID owner) throws SQLException {
try (PreparedStatement statement = database.connection().prepareStatement(
"SELECT * FROM claims WHERE owner_uuid = ? ORDER BY id")) {
statement.setString(1, owner.toString());
return list(statement);
}
}
public List<Claim> listAll() throws SQLException {
try (PreparedStatement statement = database.connection().prepareStatement(
"SELECT * FROM claims ORDER BY id")) {
return list(statement);
}
}
public Optional<Claim> findOwnedById(UUID owner, long id) throws SQLException {
try (PreparedStatement statement = database.connection().prepareStatement(
"SELECT * FROM claims WHERE owner_uuid = ? AND id = ?")) {
statement.setString(1, owner.toString());
statement.setLong(2, id);
try (ResultSet result = statement.executeQuery()) {
return result.next() ? Optional.of(map(result)) : Optional.empty();
}
}
}
public boolean transferOwned(UUID owner, long id, UUID newOwner) throws SQLException {
try (PreparedStatement statement = database.connection().prepareStatement(
"UPDATE claims SET owner_uuid = ? WHERE id = ? AND owner_uuid = ?")) {
statement.setString(1, newOwner.toString());
statement.setLong(2, id);
statement.setString(3, owner.toString());
return statement.executeUpdate() == 1;
}
}
public boolean overlaps(Cuboid2D area) throws SQLException {
try (PreparedStatement statement = database.connection().prepareStatement("""
SELECT 1 FROM claims
WHERE world = ? AND x1 <= ? AND x2 >= ? AND z1 <= ? AND z2 >= ?
LIMIT 1
""")) {
statement.setString(1, area.world());
statement.setInt(2, area.x2());
statement.setInt(3, area.x1());
statement.setInt(4, area.z2());
statement.setInt(5, area.z1());
try (ResultSet result = statement.executeQuery()) {
return result.next();
}
}
}
private List<Claim> list(PreparedStatement statement) throws SQLException {
List<Claim> claims = new ArrayList<>();
try (ResultSet result = statement.executeQuery()) {
while (result.next()) {
claims.add(map(result));
}
}
return claims;
}
private Claim map(ResultSet result) throws SQLException {
long expiresAt = result.getLong("expires_at");
Long nullableExpiresAt = result.wasNull() ? null : expiresAt;
return new Claim(
result.getLong("id"),
UUID.fromString(result.getString("owner_uuid")),
result.getString("world"),
result.getInt("x1"),
result.getInt("z1"),
result.getInt("x2"),
result.getInt("z2"),
result.getString("claim_type"),
nullableExpiresAt,
result.getLong("tax_due"),
result.getLong("created_at"),
result.getLong("purchase_cost"),
vertices(result.getLong("id"))
);
}
private void insertVertices(long claimId, List<ClaimPoint> vertices) throws SQLException {
if (claimId <= 0L || vertices == null || vertices.isEmpty()) {
return;
}
try (PreparedStatement insert = database.connection().prepareStatement("""
INSERT INTO claim_vertices(claim_id, vertex_order, x, z)
VALUES (?, ?, ?, ?)
""")) {
for (int i = 0; i < vertices.size(); i++) {
ClaimPoint point = vertices.get(i);
insert.setLong(1, claimId);
insert.setInt(2, i);
insert.setInt(3, point.x());
insert.setInt(4, point.z());
insert.addBatch();
}
insert.executeBatch();
}
}
private List<ClaimPoint> vertices(long claimId) throws SQLException {
List<ClaimPoint> points = new ArrayList<>();
try (PreparedStatement statement = database.connection().prepareStatement(
"SELECT x, z FROM claim_vertices WHERE claim_id = ? ORDER BY vertex_order")) {
statement.setLong(1, claimId);
try (ResultSet result = statement.executeQuery()) {
while (result.next()) {
points.add(new ClaimPoint(result.getInt("x"), result.getInt("z")));
}
}
}
return points;
}
}

View File

@@ -0,0 +1,275 @@
package com.librewiki.coalgov.storage;
import java.io.File;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public final class Database {
private final File databaseFile;
private Connection connection;
public Database(File dataFolder) {
this.databaseFile = new File(dataFolder, "coalgov.db");
}
public void open() throws SQLException {
if (connection != null && !connection.isClosed()) {
return;
}
databaseFile.getParentFile().mkdirs();
connection = DriverManager.getConnection("jdbc:sqlite:" + databaseFile.getAbsolutePath());
try (Statement statement = connection.createStatement()) {
statement.execute("PRAGMA foreign_keys = ON");
statement.execute("PRAGMA journal_mode = WAL");
}
initialize();
}
public Connection connection() {
return connection;
}
public void close() {
if (connection == null) {
return;
}
try {
connection.close();
} catch (SQLException ignored) {
}
}
private void initialize() throws SQLException {
try (Statement statement = connection.createStatement()) {
statement.execute("""
CREATE TABLE IF NOT EXISTS schema_meta (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
)
""");
statement.execute("""
CREATE TABLE IF NOT EXISTS players (
uuid TEXT PRIMARY KEY,
name TEXT NOT NULL,
coal_balance INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL
)
""");
statement.execute("""
CREATE TABLE IF NOT EXISTS claims (
id INTEGER PRIMARY KEY AUTOINCREMENT,
owner_uuid TEXT NOT NULL,
world TEXT NOT NULL,
x1 INTEGER NOT NULL,
z1 INTEGER NOT NULL,
x2 INTEGER NOT NULL,
z2 INTEGER NOT NULL,
claim_type TEXT NOT NULL,
expires_at INTEGER,
tax_due INTEGER NOT NULL DEFAULT 0,
purchase_cost INTEGER NOT NULL DEFAULT -1,
created_at INTEGER NOT NULL
)
""");
statement.execute("""
CREATE TABLE IF NOT EXISTS claim_vertices (
claim_id INTEGER NOT NULL,
vertex_order INTEGER NOT NULL,
x INTEGER NOT NULL,
z INTEGER NOT NULL,
PRIMARY KEY (claim_id, vertex_order)
)
""");
statement.execute("""
CREATE TABLE IF NOT EXISTS land_regions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
world TEXT NOT NULL,
x1 INTEGER NOT NULL,
z1 INTEGER NOT NULL,
x2 INTEGER NOT NULL,
z2 INTEGER NOT NULL,
land_class TEXT NOT NULL,
created_at INTEGER NOT NULL
)
""");
statement.execute("""
CREATE TABLE IF NOT EXISTS permits (
id INTEGER PRIMARY KEY AUTOINCREMENT,
owner_uuid TEXT NOT NULL,
permit_type TEXT NOT NULL,
region_name TEXT,
expires_at INTEGER,
created_at INTEGER NOT NULL
)
""");
statement.execute("""
CREATE TABLE IF NOT EXISTS transactions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
from_uuid TEXT,
to_uuid TEXT,
amount INTEGER NOT NULL,
reason TEXT NOT NULL,
created_at INTEGER NOT NULL
)
""");
statement.execute("""
CREATE TABLE IF NOT EXISTS market_stock (
material TEXT PRIMARY KEY,
stock INTEGER NOT NULL DEFAULT 0,
updated_at INTEGER NOT NULL
)
""");
statement.execute("""
CREATE TABLE IF NOT EXISTS treasury (
id TEXT PRIMARY KEY,
balance INTEGER NOT NULL DEFAULT 0
)
""");
statement.execute("""
CREATE TABLE IF NOT EXISTS tax_exemptions (
uuid TEXT PRIMARY KEY,
exempt INTEGER NOT NULL DEFAULT 0,
updated_at INTEGER NOT NULL
)
""");
statement.execute("""
CREATE TABLE IF NOT EXISTS fines (
id INTEGER PRIMARY KEY AUTOINCREMENT,
player_uuid TEXT NOT NULL,
issuer_uuid TEXT,
amount INTEGER NOT NULL,
reason TEXT NOT NULL,
paid_at INTEGER,
created_at INTEGER NOT NULL
)
""");
statement.execute("""
CREATE TABLE IF NOT EXISTS super_furnaces (
world TEXT NOT NULL,
x INTEGER NOT NULL,
y INTEGER NOT NULL,
z INTEGER NOT NULL,
created_at INTEGER NOT NULL,
PRIMARY KEY (world, x, y, z)
)
""");
statement.execute("""
CREATE TABLE IF NOT EXISTS npc_zones (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
world TEXT NOT NULL,
x1 INTEGER NOT NULL,
z1 INTEGER NOT NULL,
x2 INTEGER NOT NULL,
z2 INTEGER NOT NULL,
created_at INTEGER NOT NULL
)
""");
statement.execute("""
CREATE TABLE IF NOT EXISTS npc_zone_vertices (
zone_id INTEGER NOT NULL,
vertex_order INTEGER NOT NULL,
x INTEGER NOT NULL,
z INTEGER NOT NULL,
PRIMARY KEY (zone_id, vertex_order)
)
""");
statement.execute("""
CREATE TABLE IF NOT EXISTS coalgov_npcs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
citizens_id INTEGER NOT NULL UNIQUE,
account_uuid TEXT NOT NULL UNIQUE,
npc_type TEXT NOT NULL,
name TEXT NOT NULL,
zone_id INTEGER NOT NULL,
world TEXT NOT NULL,
x REAL NOT NULL,
y REAL NOT NULL,
z REAL NOT NULL,
created_at INTEGER NOT NULL
)
""");
statement.execute("""
CREATE TABLE IF NOT EXISTS npc_inventory (
npc_id INTEGER NOT NULL,
material TEXT NOT NULL,
amount INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (npc_id, material)
)
""");
statement.execute("CREATE INDEX IF NOT EXISTS idx_claims_location ON claims(world, x1, z1, x2, z2)");
statement.execute("CREATE INDEX IF NOT EXISTS idx_claim_vertices_claim ON claim_vertices(claim_id)");
statement.execute("CREATE INDEX IF NOT EXISTS idx_land_location ON land_regions(world, x1, z1, x2, z2)");
statement.execute("CREATE INDEX IF NOT EXISTS idx_permits_owner ON permits(owner_uuid, permit_type, region_name)");
statement.execute("CREATE INDEX IF NOT EXISTS idx_fines_player ON fines(player_uuid, paid_at)");
statement.execute("CREATE INDEX IF NOT EXISTS idx_coalgov_npcs_citizens ON coalgov_npcs(citizens_id)");
statement.execute("CREATE INDEX IF NOT EXISTS idx_npc_zone_vertices_zone ON npc_zone_vertices(zone_id)");
}
ensureColumn("claims", "purchase_cost", "INTEGER NOT NULL DEFAULT -1");
migrateCoalBalancesToCents();
}
private void migrateCoalBalancesToCents() throws SQLException {
String version = metaValue("economy_units");
if ("coal_cents_v1".equals(version)) {
return;
}
boolean originalAutoCommit = connection.getAutoCommit();
connection.setAutoCommit(false);
try (Statement statement = connection.createStatement()) {
statement.execute("UPDATE players SET coal_balance = coal_balance * 100");
statement.execute("UPDATE transactions SET amount = amount * 100");
statement.execute("UPDATE treasury SET balance = balance * 100");
statement.execute("UPDATE fines SET amount = amount * 100");
statement.execute("UPDATE claims SET tax_due = tax_due * 100");
statement.execute("UPDATE claims SET purchase_cost = purchase_cost * 100 WHERE purchase_cost >= 0");
setMetaValue("economy_units", "coal_cents_v1");
connection.commit();
} catch (SQLException exception) {
connection.rollback();
throw exception;
} finally {
connection.setAutoCommit(originalAutoCommit);
}
}
private String metaValue(String key) throws SQLException {
try (PreparedStatement statement = connection.prepareStatement("SELECT value FROM schema_meta WHERE key = ?")) {
statement.setString(1, key);
try (ResultSet result = statement.executeQuery()) {
return result.next() ? result.getString("value") : null;
}
}
}
private void setMetaValue(String key, String value) throws SQLException {
try (PreparedStatement statement = connection.prepareStatement("""
INSERT INTO schema_meta(key, value)
VALUES (?, ?)
ON CONFLICT(key) DO UPDATE SET value = excluded.value
""")) {
statement.setString(1, key);
statement.setString(2, value);
statement.executeUpdate();
}
}
private void ensureColumn(String table, String column, String definition) throws SQLException {
try (Statement statement = connection.createStatement();
var result = statement.executeQuery("PRAGMA table_info(" + table + ")")) {
while (result.next()) {
if (column.equalsIgnoreCase(result.getString("name"))) {
return;
}
}
}
try (Statement statement = connection.createStatement()) {
statement.execute("ALTER TABLE " + table + " ADD COLUMN " + column + " " + definition);
}
}
}

View File

@@ -0,0 +1,133 @@
package com.librewiki.coalgov.storage;
import org.bukkit.OfflinePlayer;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.UUID;
public final class EconomyRepository {
private final Database database;
public EconomyRepository(Database database) {
this.database = database;
}
public void ensurePlayer(OfflinePlayer player, long startingBalance) throws SQLException {
String name = player.getName() == null ? player.getUniqueId().toString() : player.getName();
ensureAccount(player.getUniqueId(), name, startingBalance);
}
public void ensureAccount(UUID uuid, String name, long startingBalance) throws SQLException {
try (PreparedStatement insert = database.connection().prepareStatement("""
INSERT INTO players(uuid, name, coal_balance, created_at)
VALUES (?, ?, ?, ?)
ON CONFLICT(uuid) DO UPDATE SET name = excluded.name
""")) {
insert.setString(1, uuid.toString());
insert.setString(2, name);
insert.setLong(3, startingBalance);
insert.setLong(4, System.currentTimeMillis());
insert.executeUpdate();
}
}
public long balance(UUID uuid) throws SQLException {
try (PreparedStatement statement = database.connection().prepareStatement(
"SELECT coal_balance FROM players WHERE uuid = ?")) {
statement.setString(1, uuid.toString());
try (ResultSet result = statement.executeQuery()) {
return result.next() ? result.getLong("coal_balance") : 0L;
}
}
}
public boolean add(UUID uuid, long amount, UUID fromUuid, UUID toUuid, String reason) throws SQLException {
if (amount <= 0) {
return false;
}
try (PreparedStatement update = database.connection().prepareStatement(
"UPDATE players SET coal_balance = coal_balance + ? WHERE uuid = ?")) {
update.setLong(1, amount);
update.setString(2, uuid.toString());
if (update.executeUpdate() != 1) {
return false;
}
}
recordTransaction(fromUuid, toUuid, amount, reason);
return true;
}
public boolean subtract(UUID uuid, long amount, UUID fromUuid, UUID toUuid, String reason) throws SQLException {
if (amount <= 0) {
return false;
}
try (PreparedStatement update = database.connection().prepareStatement("""
UPDATE players SET coal_balance = coal_balance - ?
WHERE uuid = ? AND coal_balance >= ?
""")) {
update.setLong(1, amount);
update.setString(2, uuid.toString());
update.setLong(3, amount);
if (update.executeUpdate() != 1) {
return false;
}
}
recordTransaction(fromUuid, toUuid, amount, reason);
return true;
}
public boolean transfer(UUID from, UUID to, long amount, String reason) throws SQLException {
if (amount <= 0 || from.equals(to)) {
return false;
}
boolean originalAutoCommit = database.connection().getAutoCommit();
database.connection().setAutoCommit(false);
try {
try (PreparedStatement debit = database.connection().prepareStatement("""
UPDATE players SET coal_balance = coal_balance - ?
WHERE uuid = ? AND coal_balance >= ?
""")) {
debit.setLong(1, amount);
debit.setString(2, from.toString());
debit.setLong(3, amount);
if (debit.executeUpdate() != 1) {
database.connection().rollback();
return false;
}
}
try (PreparedStatement credit = database.connection().prepareStatement(
"UPDATE players SET coal_balance = coal_balance + ? WHERE uuid = ?")) {
credit.setLong(1, amount);
credit.setString(2, to.toString());
if (credit.executeUpdate() != 1) {
database.connection().rollback();
return false;
}
}
recordTransaction(from, to, amount, reason);
database.connection().commit();
return true;
} catch (SQLException exception) {
database.connection().rollback();
throw exception;
} finally {
database.connection().setAutoCommit(originalAutoCommit);
}
}
private void recordTransaction(UUID fromUuid, UUID toUuid, long amount, String reason) throws SQLException {
try (PreparedStatement insert = database.connection().prepareStatement("""
INSERT INTO transactions(from_uuid, to_uuid, amount, reason, created_at)
VALUES (?, ?, ?, ?, ?)
""")) {
insert.setString(1, fromUuid == null ? null : fromUuid.toString());
insert.setString(2, toUuid == null ? null : toUuid.toString());
insert.setLong(3, amount);
insert.setString(4, reason);
insert.setLong(5, System.currentTimeMillis());
insert.executeUpdate();
}
}
}

View File

@@ -0,0 +1,171 @@
package com.librewiki.coalgov.storage;
import com.librewiki.coalgov.model.Fine;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public final class GovernmentRepository {
private static final String TREASURY_ID = "main";
private final Database database;
public GovernmentRepository(Database database) {
this.database = database;
}
public long treasuryBalance() throws SQLException {
ensureTreasury();
try (PreparedStatement statement = database.connection().prepareStatement(
"SELECT balance FROM treasury WHERE id = ?")) {
statement.setString(1, TREASURY_ID);
try (ResultSet result = statement.executeQuery()) {
return result.next() ? result.getLong("balance") : 0L;
}
}
}
public void addTreasury(long amount) throws SQLException {
if (amount <= 0L) {
return;
}
try (PreparedStatement statement = database.connection().prepareStatement("""
INSERT INTO treasury(id, balance)
VALUES (?, ?)
ON CONFLICT(id) DO UPDATE SET balance = balance + excluded.balance
""")) {
statement.setString(1, TREASURY_ID);
statement.setLong(2, amount);
statement.executeUpdate();
}
}
public boolean spendTreasury(long amount) throws SQLException {
if (amount <= 0L) {
return false;
}
ensureTreasury();
try (PreparedStatement statement = database.connection().prepareStatement("""
UPDATE treasury SET balance = balance - ?
WHERE id = ? AND balance >= ?
""")) {
statement.setLong(1, amount);
statement.setString(2, TREASURY_ID);
statement.setLong(3, amount);
return statement.executeUpdate() == 1;
}
}
public void setTaxExempt(UUID uuid, boolean exempt) throws SQLException {
try (PreparedStatement statement = database.connection().prepareStatement("""
INSERT INTO tax_exemptions(uuid, exempt, updated_at)
VALUES (?, ?, ?)
ON CONFLICT(uuid) DO UPDATE SET exempt = excluded.exempt, updated_at = excluded.updated_at
""")) {
statement.setString(1, uuid.toString());
statement.setInt(2, exempt ? 1 : 0);
statement.setLong(3, System.currentTimeMillis());
statement.executeUpdate();
}
}
public boolean taxExempt(UUID uuid) throws SQLException {
try (PreparedStatement statement = database.connection().prepareStatement(
"SELECT exempt FROM tax_exemptions WHERE uuid = ?")) {
statement.setString(1, uuid.toString());
try (ResultSet result = statement.executeQuery()) {
return result.next() && result.getInt("exempt") == 1;
}
}
}
public long createFine(UUID playerUuid, UUID issuerUuid, long amount, String reason) throws SQLException {
try (PreparedStatement statement = database.connection().prepareStatement("""
INSERT INTO fines(player_uuid, issuer_uuid, amount, reason, created_at)
VALUES (?, ?, ?, ?, ?)
""", Statement.RETURN_GENERATED_KEYS)) {
statement.setString(1, playerUuid.toString());
statement.setString(2, issuerUuid == null ? null : issuerUuid.toString());
statement.setLong(3, amount);
statement.setString(4, reason);
statement.setLong(5, System.currentTimeMillis());
statement.executeUpdate();
try (ResultSet keys = statement.getGeneratedKeys()) {
return keys.next() ? keys.getLong(1) : -1L;
}
}
}
public List<Fine> unpaidFines(UUID playerUuid) throws SQLException {
try (PreparedStatement statement = database.connection().prepareStatement("""
SELECT * FROM fines
WHERE player_uuid = ? AND paid_at IS NULL
ORDER BY id
""")) {
statement.setString(1, playerUuid.toString());
return list(statement);
}
}
public Optional<Fine> findUnpaidFine(UUID playerUuid, long id) throws SQLException {
try (PreparedStatement statement = database.connection().prepareStatement("""
SELECT * FROM fines
WHERE player_uuid = ? AND id = ? AND paid_at IS NULL
""")) {
statement.setString(1, playerUuid.toString());
statement.setLong(2, id);
try (ResultSet result = statement.executeQuery()) {
return result.next() ? Optional.of(map(result)) : Optional.empty();
}
}
}
public boolean markFinePaid(long id) throws SQLException {
try (PreparedStatement statement = database.connection().prepareStatement("""
UPDATE fines SET paid_at = ?
WHERE id = ? AND paid_at IS NULL
""")) {
statement.setLong(1, System.currentTimeMillis());
statement.setLong(2, id);
return statement.executeUpdate() == 1;
}
}
private void ensureTreasury() throws SQLException {
try (PreparedStatement statement = database.connection().prepareStatement(
"INSERT INTO treasury(id, balance) VALUES (?, 0) ON CONFLICT(id) DO NOTHING")) {
statement.setString(1, TREASURY_ID);
statement.executeUpdate();
}
}
private List<Fine> list(PreparedStatement statement) throws SQLException {
List<Fine> fines = new ArrayList<>();
try (ResultSet result = statement.executeQuery()) {
while (result.next()) {
fines.add(map(result));
}
}
return fines;
}
private Fine map(ResultSet result) throws SQLException {
String issuer = result.getString("issuer_uuid");
long paidAt = result.getLong("paid_at");
boolean paidAtNull = result.wasNull();
return new Fine(
result.getLong("id"),
UUID.fromString(result.getString("player_uuid")),
issuer == null ? null : UUID.fromString(issuer),
result.getLong("amount"),
result.getString("reason"),
paidAtNull ? null : paidAt,
result.getLong("created_at")
);
}
}

View File

@@ -0,0 +1,123 @@
package com.librewiki.coalgov.storage;
import com.librewiki.coalgov.model.LandClass;
import com.librewiki.coalgov.model.LandRegion;
import com.librewiki.coalgov.util.Cuboid2D;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public final class LandRepository {
private final Database database;
public LandRepository(Database database) {
this.database = database;
}
public boolean create(String name, Cuboid2D area, LandClass landClass) throws SQLException {
try (PreparedStatement insert = database.connection().prepareStatement("""
INSERT INTO land_regions(name, world, x1, z1, x2, z2, land_class, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""")) {
insert.setString(1, name);
insert.setString(2, area.world());
insert.setInt(3, area.x1());
insert.setInt(4, area.z1());
insert.setInt(5, area.x2());
insert.setInt(6, area.z2());
insert.setString(7, landClass.name());
insert.setLong(8, System.currentTimeMillis());
return insert.executeUpdate() == 1;
}
}
public boolean delete(String name) throws SQLException {
try (PreparedStatement delete = database.connection().prepareStatement(
"DELETE FROM land_regions WHERE lower(name) = lower(?)")) {
delete.setString(1, name);
return delete.executeUpdate() == 1;
}
}
public Optional<LandRegion> findByName(String name) throws SQLException {
try (PreparedStatement statement = database.connection().prepareStatement(
"SELECT * FROM land_regions WHERE lower(name) = lower(?)")) {
statement.setString(1, name);
try (ResultSet result = statement.executeQuery()) {
return result.next() ? Optional.of(map(result)) : Optional.empty();
}
}
}
public Optional<LandRegion> findAt(String world, int x, int z) throws SQLException {
try (PreparedStatement statement = database.connection().prepareStatement("""
SELECT * FROM land_regions
WHERE world = ? AND x1 <= ? AND x2 >= ? AND z1 <= ? AND z2 >= ?
ORDER BY id DESC LIMIT 1
""")) {
statement.setString(1, world);
statement.setInt(2, x);
statement.setInt(3, x);
statement.setInt(4, z);
statement.setInt(5, z);
try (ResultSet result = statement.executeQuery()) {
return result.next() ? Optional.of(map(result)) : Optional.empty();
}
}
}
public List<LandRegion> listAll() throws SQLException {
try (Statement statement = database.connection().createStatement();
ResultSet result = statement.executeQuery("SELECT * FROM land_regions ORDER BY name")) {
List<LandRegion> regions = new ArrayList<>();
while (result.next()) {
regions.add(map(result));
}
return regions;
}
}
public boolean overlaps(Cuboid2D area, LandClass... blockedClasses) throws SQLException {
StringBuilder sql = new StringBuilder("""
SELECT land_class FROM land_regions
WHERE world = ? AND x1 <= ? AND x2 >= ? AND z1 <= ? AND z2 >= ?
""");
try (PreparedStatement statement = database.connection().prepareStatement(sql.toString())) {
statement.setString(1, area.world());
statement.setInt(2, area.x2());
statement.setInt(3, area.x1());
statement.setInt(4, area.z2());
statement.setInt(5, area.z1());
try (ResultSet result = statement.executeQuery()) {
while (result.next()) {
LandClass actual = LandClass.valueOf(result.getString("land_class"));
for (LandClass blocked : blockedClasses) {
if (actual == blocked) {
return true;
}
}
}
}
}
return false;
}
private LandRegion map(ResultSet result) throws SQLException {
return new LandRegion(
result.getLong("id"),
result.getString("name"),
result.getString("world"),
result.getInt("x1"),
result.getInt("z1"),
result.getInt("x2"),
result.getInt("z2"),
LandClass.valueOf(result.getString("land_class")),
result.getLong("created_at")
);
}
}

View File

@@ -0,0 +1,69 @@
package com.librewiki.coalgov.storage;
import org.bukkit.Material;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public final class MarketRepository {
private final Database database;
public MarketRepository(Database database) {
this.database = database;
}
public long stock(Material material, long initialStock) throws SQLException {
try (PreparedStatement insert = database.connection().prepareStatement("""
INSERT INTO market_stock(material, stock, updated_at)
VALUES (?, ?, ?)
ON CONFLICT(material) DO NOTHING
""")) {
insert.setString(1, material.name());
insert.setLong(2, Math.max(0L, initialStock));
insert.setLong(3, System.currentTimeMillis());
insert.executeUpdate();
}
try (PreparedStatement select = database.connection().prepareStatement(
"SELECT stock FROM market_stock WHERE material = ?")) {
select.setString(1, material.name());
try (ResultSet result = select.executeQuery()) {
return result.next() ? result.getLong("stock") : 0L;
}
}
}
public void addStock(Material material, long amount) throws SQLException {
if (amount <= 0) {
return;
}
try (PreparedStatement statement = database.connection().prepareStatement("""
INSERT INTO market_stock(material, stock, updated_at)
VALUES (?, ?, ?)
ON CONFLICT(material) DO UPDATE SET
stock = stock + excluded.stock,
updated_at = excluded.updated_at
""")) {
statement.setString(1, material.name());
statement.setLong(2, amount);
statement.setLong(3, System.currentTimeMillis());
statement.executeUpdate();
}
}
public boolean removeStock(Material material, long amount) throws SQLException {
if (amount <= 0) {
return false;
}
try (PreparedStatement statement = database.connection().prepareStatement("""
UPDATE market_stock SET stock = stock - ?, updated_at = ?
WHERE material = ? AND stock >= ?
""")) {
statement.setLong(1, amount);
statement.setLong(2, System.currentTimeMillis());
statement.setString(3, material.name());
statement.setLong(4, amount);
return statement.executeUpdate() == 1;
}
}
}

View File

@@ -0,0 +1,270 @@
package com.librewiki.coalgov.storage;
import com.librewiki.coalgov.model.ClaimPoint;
import com.librewiki.coalgov.model.CoalNpc;
import com.librewiki.coalgov.model.NpcZone;
import org.bukkit.Material;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
public final class NpcRepository {
private final Database database;
public NpcRepository(Database database) {
this.database = database;
}
public long createZone(String name, String world, List<ClaimPoint> vertices) throws SQLException {
int x1 = vertices.stream().mapToInt(ClaimPoint::x).min().orElse(0);
int x2 = vertices.stream().mapToInt(ClaimPoint::x).max().orElse(0);
int z1 = vertices.stream().mapToInt(ClaimPoint::z).min().orElse(0);
int z2 = vertices.stream().mapToInt(ClaimPoint::z).max().orElse(0);
boolean originalAutoCommit = database.connection().getAutoCommit();
database.connection().setAutoCommit(false);
try (PreparedStatement insert = database.connection().prepareStatement("""
INSERT INTO npc_zones(name, world, x1, z1, x2, z2, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
""", Statement.RETURN_GENERATED_KEYS)) {
insert.setString(1, name);
insert.setString(2, world);
insert.setInt(3, x1);
insert.setInt(4, z1);
insert.setInt(5, x2);
insert.setInt(6, z2);
insert.setLong(7, System.currentTimeMillis());
insert.executeUpdate();
try (ResultSet keys = insert.getGeneratedKeys()) {
long id = keys.next() ? keys.getLong(1) : -1L;
insertVertices(id, vertices);
database.connection().commit();
return id;
}
} catch (SQLException exception) {
database.connection().rollback();
throw exception;
} finally {
database.connection().setAutoCommit(originalAutoCommit);
}
}
public Optional<NpcZone> findZone(String name) throws SQLException {
try (PreparedStatement statement = database.connection().prepareStatement(
"SELECT * FROM npc_zones WHERE name = ?")) {
statement.setString(1, name);
try (ResultSet result = statement.executeQuery()) {
return result.next() ? Optional.of(mapZone(result)) : Optional.empty();
}
}
}
public Optional<NpcZone> findZone(long id) throws SQLException {
try (PreparedStatement statement = database.connection().prepareStatement(
"SELECT * FROM npc_zones WHERE id = ?")) {
statement.setLong(1, id);
try (ResultSet result = statement.executeQuery()) {
return result.next() ? Optional.of(mapZone(result)) : Optional.empty();
}
}
}
public List<NpcZone> listZones() throws SQLException {
try (PreparedStatement statement = database.connection().prepareStatement("SELECT * FROM npc_zones ORDER BY id")) {
List<NpcZone> zones = new ArrayList<>();
try (ResultSet result = statement.executeQuery()) {
while (result.next()) {
zones.add(mapZone(result));
}
}
return zones;
}
}
public long createNpc(int citizensId, UUID accountUuid, String type, String name, long zoneId,
String world, double x, double y, double z) throws SQLException {
try (PreparedStatement statement = database.connection().prepareStatement("""
INSERT INTO coalgov_npcs(citizens_id, account_uuid, npc_type, name, zone_id, world, x, y, z, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", Statement.RETURN_GENERATED_KEYS)) {
statement.setInt(1, citizensId);
statement.setString(2, accountUuid.toString());
statement.setString(3, type.toUpperCase());
statement.setString(4, name);
statement.setLong(5, zoneId);
statement.setString(6, world);
statement.setDouble(7, x);
statement.setDouble(8, y);
statement.setDouble(9, z);
statement.setLong(10, System.currentTimeMillis());
statement.executeUpdate();
try (ResultSet keys = statement.getGeneratedKeys()) {
return keys.next() ? keys.getLong(1) : -1L;
}
}
}
public Optional<CoalNpc> findNpc(long id) throws SQLException {
try (PreparedStatement statement = database.connection().prepareStatement("SELECT * FROM coalgov_npcs WHERE id = ?")) {
statement.setLong(1, id);
try (ResultSet result = statement.executeQuery()) {
return result.next() ? Optional.of(mapNpc(result)) : Optional.empty();
}
}
}
public Optional<CoalNpc> findByCitizensId(int citizensId) throws SQLException {
try (PreparedStatement statement = database.connection().prepareStatement("SELECT * FROM coalgov_npcs WHERE citizens_id = ?")) {
statement.setInt(1, citizensId);
try (ResultSet result = statement.executeQuery()) {
return result.next() ? Optional.of(mapNpc(result)) : Optional.empty();
}
}
}
public List<CoalNpc> listNpcs() throws SQLException {
try (PreparedStatement statement = database.connection().prepareStatement("SELECT * FROM coalgov_npcs ORDER BY id")) {
List<CoalNpc> npcs = new ArrayList<>();
try (ResultSet result = statement.executeQuery()) {
while (result.next()) {
npcs.add(mapNpc(result));
}
}
return npcs;
}
}
public boolean deleteNpc(long id) throws SQLException {
try (PreparedStatement statement = database.connection().prepareStatement("DELETE FROM coalgov_npcs WHERE id = ?")) {
statement.setLong(1, id);
return statement.executeUpdate() == 1;
}
}
public void addInventory(long npcId, Material material, long amount) throws SQLException {
if (amount <= 0L) {
return;
}
try (PreparedStatement statement = database.connection().prepareStatement("""
INSERT INTO npc_inventory(npc_id, material, amount)
VALUES (?, ?, ?)
ON CONFLICT(npc_id, material) DO UPDATE SET amount = amount + excluded.amount
""")) {
statement.setLong(1, npcId);
statement.setString(2, material.name());
statement.setLong(3, amount);
statement.executeUpdate();
}
}
public boolean removeInventory(long npcId, Material material, long amount) throws SQLException {
if (amount <= 0L) {
return false;
}
try (PreparedStatement statement = database.connection().prepareStatement("""
UPDATE npc_inventory SET amount = amount - ?
WHERE npc_id = ? AND material = ? AND amount >= ?
""")) {
statement.setLong(1, amount);
statement.setLong(2, npcId);
statement.setString(3, material.name());
statement.setLong(4, amount);
return statement.executeUpdate() == 1;
}
}
public long inventoryAmount(long npcId, Material material) throws SQLException {
try (PreparedStatement statement = database.connection().prepareStatement(
"SELECT amount FROM npc_inventory WHERE npc_id = ? AND material = ?")) {
statement.setLong(1, npcId);
statement.setString(2, material.name());
try (ResultSet result = statement.executeQuery()) {
return result.next() ? result.getLong("amount") : 0L;
}
}
}
public Map<Material, Long> inventory(long npcId) throws SQLException {
try (PreparedStatement statement = database.connection().prepareStatement(
"SELECT material, amount FROM npc_inventory WHERE npc_id = ? AND amount > 0 ORDER BY material")) {
statement.setLong(1, npcId);
Map<Material, Long> inventory = new LinkedHashMap<>();
try (ResultSet result = statement.executeQuery()) {
while (result.next()) {
Material material = Material.matchMaterial(result.getString("material"));
if (material != null) {
inventory.put(material, result.getLong("amount"));
}
}
}
return inventory;
}
}
private void insertVertices(long zoneId, List<ClaimPoint> vertices) throws SQLException {
try (PreparedStatement insert = database.connection().prepareStatement("""
INSERT INTO npc_zone_vertices(zone_id, vertex_order, x, z)
VALUES (?, ?, ?, ?)
""")) {
for (int index = 0; index < vertices.size(); index++) {
ClaimPoint point = vertices.get(index);
insert.setLong(1, zoneId);
insert.setInt(2, index);
insert.setInt(3, point.x());
insert.setInt(4, point.z());
insert.addBatch();
}
insert.executeBatch();
}
}
private NpcZone mapZone(ResultSet result) throws SQLException {
long id = result.getLong("id");
return new NpcZone(
id,
result.getString("name"),
result.getString("world"),
result.getInt("x1"),
result.getInt("z1"),
result.getInt("x2"),
result.getInt("z2"),
vertices(id)
);
}
private CoalNpc mapNpc(ResultSet result) throws SQLException {
return new CoalNpc(
result.getLong("id"),
result.getInt("citizens_id"),
UUID.fromString(result.getString("account_uuid")),
result.getString("npc_type"),
result.getString("name"),
result.getLong("zone_id"),
result.getString("world"),
result.getDouble("x"),
result.getDouble("y"),
result.getDouble("z")
);
}
private List<ClaimPoint> vertices(long zoneId) throws SQLException {
List<ClaimPoint> points = new ArrayList<>();
try (PreparedStatement statement = database.connection().prepareStatement(
"SELECT x, z FROM npc_zone_vertices WHERE zone_id = ? ORDER BY vertex_order")) {
statement.setLong(1, zoneId);
try (ResultSet result = statement.executeQuery()) {
while (result.next()) {
points.add(new ClaimPoint(result.getInt("x"), result.getInt("z")));
}
}
}
return points;
}
}

View File

@@ -0,0 +1,89 @@
package com.librewiki.coalgov.storage;
import com.librewiki.coalgov.model.Permit;
import com.librewiki.coalgov.model.PermitType;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public final class PermitRepository {
private final Database database;
public PermitRepository(Database database) {
this.database = database;
}
public long create(UUID owner, PermitType permitType, String regionName, Long expiresAt) throws SQLException {
try (PreparedStatement insert = database.connection().prepareStatement("""
INSERT INTO permits(owner_uuid, permit_type, region_name, expires_at, created_at)
VALUES (?, ?, ?, ?, ?)
""", Statement.RETURN_GENERATED_KEYS)) {
insert.setString(1, owner.toString());
insert.setString(2, permitType.name());
insert.setString(3, regionName);
if (expiresAt == null) {
insert.setObject(4, null);
} else {
insert.setLong(4, expiresAt);
}
insert.setLong(5, System.currentTimeMillis());
insert.executeUpdate();
try (ResultSet keys = insert.getGeneratedKeys()) {
return keys.next() ? keys.getLong(1) : -1L;
}
}
}
public boolean hasActive(UUID owner, PermitType permitType, String regionName, long now) throws SQLException {
try (PreparedStatement statement = database.connection().prepareStatement("""
SELECT 1 FROM permits
WHERE owner_uuid = ? AND permit_type = ? AND (region_name IS NULL OR lower(region_name) = lower(?))
AND (expires_at IS NULL OR expires_at > ?)
LIMIT 1
""")) {
statement.setString(1, owner.toString());
statement.setString(2, permitType.name());
statement.setString(3, regionName);
statement.setLong(4, now);
try (ResultSet result = statement.executeQuery()) {
return result.next();
}
}
}
public List<Permit> listActive(UUID owner, long now) throws SQLException {
try (PreparedStatement statement = database.connection().prepareStatement("""
SELECT * FROM permits
WHERE owner_uuid = ? AND (expires_at IS NULL OR expires_at > ?)
ORDER BY id
""")) {
statement.setString(1, owner.toString());
statement.setLong(2, now);
List<Permit> permits = new ArrayList<>();
try (ResultSet result = statement.executeQuery()) {
while (result.next()) {
permits.add(map(result));
}
}
return permits;
}
}
private Permit map(ResultSet result) throws SQLException {
long expiresAt = result.getLong("expires_at");
Long nullableExpiresAt = result.wasNull() ? null : expiresAt;
return new Permit(
result.getLong("id"),
UUID.fromString(result.getString("owner_uuid")),
PermitType.valueOf(result.getString("permit_type")),
result.getString("region_name"),
nullableExpiresAt,
result.getLong("created_at")
);
}
}

View File

@@ -0,0 +1,55 @@
package com.librewiki.coalgov.storage;
import org.bukkit.Location;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public final class SuperFurnaceRepository {
private final Database database;
public SuperFurnaceRepository(Database database) {
this.database = database;
}
public void add(Location location) throws SQLException {
try (PreparedStatement statement = database.connection().prepareStatement("""
INSERT INTO super_furnaces(world, x, y, z, created_at)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(world, x, y, z) DO NOTHING
""")) {
bindLocation(statement, location);
statement.setLong(5, System.currentTimeMillis());
statement.executeUpdate();
}
}
public void remove(Location location) throws SQLException {
try (PreparedStatement statement = database.connection().prepareStatement("""
DELETE FROM super_furnaces
WHERE world = ? AND x = ? AND y = ? AND z = ?
""")) {
bindLocation(statement, location);
statement.executeUpdate();
}
}
public boolean exists(Location location) throws SQLException {
try (PreparedStatement statement = database.connection().prepareStatement("""
SELECT 1 FROM super_furnaces
WHERE world = ? AND x = ? AND y = ? AND z = ?
""")) {
bindLocation(statement, location);
try (var result = statement.executeQuery()) {
return result.next();
}
}
}
private void bindLocation(PreparedStatement statement, Location location) throws SQLException {
statement.setString(1, location.getWorld().getName());
statement.setInt(2, location.getBlockX());
statement.setInt(3, location.getBlockY());
statement.setInt(4, location.getBlockZ());
}
}

View File

@@ -0,0 +1,69 @@
package com.librewiki.coalgov.util;
import java.math.BigDecimal;
import java.math.RoundingMode;
public final class CoalMoney {
public static final long CENTS_PER_COAL = 100L;
private CoalMoney() {
}
public static long fromCoal(double coal) {
return Math.round(coal * CENTS_PER_COAL);
}
public static long fromCoalConfig(double coal) {
return Math.max(0L, fromCoal(coal));
}
public static String format(long cents) {
boolean negative = cents < 0L;
long absolute = Math.abs(cents);
long coal = absolute / CENTS_PER_COAL;
long coalCents = absolute % CENTS_PER_COAL;
String prefix = negative ? "-" : "";
if (coal > 0L && coalCents > 0L) {
return prefix + coal + " coal " + coalCents + "cc";
}
if (coal > 0L) {
return prefix + coal + " coal";
}
return prefix + coalCents + "cc";
}
public static long parsePositive(String raw) {
long amount = parse(raw);
return amount > 0L ? amount : -1L;
}
public static long parse(String raw) {
if (raw == null) {
return -1L;
}
String normalized = raw.trim().toLowerCase();
if (normalized.isEmpty() || normalized.startsWith("-")) {
return -1L;
}
try {
if (normalized.endsWith("cc")) {
return Long.parseLong(normalized.substring(0, normalized.length() - 2));
}
if (normalized.endsWith("c")) {
return parseDecimalCoal(normalized.substring(0, normalized.length() - 1));
}
if (normalized.endsWith("coal")) {
return parseDecimalCoal(normalized.substring(0, normalized.length() - 4));
}
return parseDecimalCoal(normalized);
} catch (ArithmeticException | NumberFormatException exception) {
return -1L;
}
}
private static long parseDecimalCoal(String raw) {
BigDecimal coal = new BigDecimal(raw.trim());
BigDecimal cents = coal.multiply(BigDecimal.valueOf(CENTS_PER_COAL));
return cents.setScale(0, RoundingMode.HALF_UP).longValueExact();
}
}

View File

@@ -0,0 +1,30 @@
package com.librewiki.coalgov.util;
public record Cuboid2D(String world, int x1, int z1, int x2, int z2) {
public Cuboid2D {
int minX = Math.min(x1, x2);
int maxX = Math.max(x1, x2);
int minZ = Math.min(z1, z2);
int maxZ = Math.max(z1, z2);
x1 = minX;
x2 = maxX;
z1 = minZ;
z2 = maxZ;
}
public boolean contains(String worldName, int x, int z) {
return world.equals(worldName) && x >= x1 && x <= x2 && z >= z1 && z <= z2;
}
public boolean overlaps(Cuboid2D other) {
return world.equals(other.world)
&& x1 <= other.x2
&& x2 >= other.x1
&& z1 <= other.z2
&& z2 >= other.z1;
}
public long area() {
return (long) (x2 - x1 + 1) * (z2 - z1 + 1);
}
}

View File

@@ -0,0 +1,29 @@
package com.librewiki.coalgov.util;
import org.bukkit.ChatColor;
import org.bukkit.configuration.file.FileConfiguration;
public final class Messages {
private final FileConfiguration config;
public Messages(FileConfiguration config) {
this.config = config;
}
public String prefix() {
return color(config.getString("messages.prefix", "&8[&6CoalGov&8]&r "));
}
public String text(String message) {
return prefix() + color(message);
}
public String raw(String message) {
return color(message);
}
@SuppressWarnings("deprecation")
private String color(String message) {
return ChatColor.translateAlternateColorCodes('&', message);
}
}

View File

@@ -0,0 +1,152 @@
economy:
starting_balance: 0
deposit_coal: true
deposit_coal_blocks: true
government:
taxes:
market_sale_percent: 5.0
market_purchase_percent: 5.0
npc:
ai:
enabled: true
api_key: ""
model: "openai/gpt-4.1-mini"
timeout_ms: 4000
trader:
max_discount_percent: 15
max_surcharge_percent: 20
worker:
tick_interval_seconds: 20
produce_material: BREAD
produce_amount: 2
claims:
resale:
min_condition_multiplier: 0.25
resource_values:
COBBLESTONE: 0.02
COAL: 1.0
COAL_BLOCK: 9.0
RAW_COPPER: 0.5
COPPER_INGOT: 1.0
RAW_IRON: 1.0
IRON_INGOT: 2.0
RAW_GOLD: 2.0
GOLD_INGOT: 4.0
DIAMOND: 32.0
EMERALD: 12.0
homestead:
base_cost: 32
cost_per_block: 0.05
max_radius: 45
first_claim:
free: true
max_radius: 12
industrial:
base_cost: 128
cost_per_block: 0.15
max_radius: 96
permits:
mining:
cost: 64
duration_hours: 72
divining_rod:
scan_radius: 4
scan_depth: 48
cooldown_seconds: 5
market:
dynamic:
initial_stock: 1024
target_stock: 1024
min_multiplier: 0.35
max_multiplier: 3.0
sell_multiplier: 0.85
buy_multiplier: 1.15
initial_stock:
WHEAT: 0
BREAD: 0
APPLE: 0
CARROT: 0
POTATO: 0
BAKED_POTATO: 0
BEETROOT: 0
MELON_SLICE: 0
SWEET_BERRIES: 0
GLOW_BERRIES: 0
COOKIE: 0
PUMPKIN_PIE: 0
COOKED_CHICKEN: 0
COOKED_PORKCHOP: 0
COOKED_BEEF: 0
COOKED_MUTTON: 0
COOKED_RABBIT: 0
COOKED_COD: 0
prices:
DIRT: 0.01
COBBLESTONE: 0.02
SAND: 0.05
GRAVEL: 0.04
OAK_LOG: 0.25
SPRUCE_LOG: 0.25
BIRCH_LOG: 0.25
TORCH: 0.10
COAL: 1.0
COAL_BLOCK: 9.0
RAW_COPPER: 0.5
COPPER_INGOT: 1.0
RAW_IRON: 1.0
IRON_INGOT: 2.0
RAW_GOLD: 2.0
GOLD_INGOT: 4.0
DIAMOND: 32.0
EMERALD: 12.0
WHEAT: 0.35
BREAD: 1.25
APPLE: 0.75
CARROT: 0.25
POTATO: 0.20
BAKED_POTATO: 0.45
BEETROOT: 0.20
MELON_SLICE: 0.15
SWEET_BERRIES: 0.20
GLOW_BERRIES: 0.35
COOKIE: 0.25
PUMPKIN_PIE: 1.0
COOKED_CHICKEN: 1.25
COOKED_PORKCHOP: 1.5
COOKED_BEEF: 1.75
COOKED_MUTTON: 1.5
COOKED_RABBIT: 1.25
COOKED_COD: 1.0
land:
default_land_class: FREEHOLD
deny_mining_in_government: true
deny_mining_in_protected_preserve: true
deny_mining_in_border_zone: true
spawn:
protection_radius: 32
guide:
create_government_region: true
lines:
- "COALGOV BASIN"
- "Free first"
- "homestead:"
- "/claim buy"
- "Use /claim wand"
- "for polygons"
- "Coal seams:"
- "redwood ridges"
- "Permits:"
- "/permit buy"
- "mining <region>"
- "/market trades"
messages:
prefix: "&8[&6CoalGov&8]&r "

View File

@@ -0,0 +1,70 @@
name: CoalGov
version: 1.0.0
main: com.librewiki.coalgov.CoalGovPlugin
api-version: '1.21'
author: LibreWiki
description: Coal-backed land, claims, permits, and mining governance.
depend: [Citizens]
commands:
coal:
description: CoalGov economy commands.
usage: /coal <balance|deposit|withdraw|pay|fines>
permission: coalgov.coal
claim:
description: CoalGov claim commands.
usage: /claim <wand|buy|info|list|show|appraise|sell|transfer|abandon>
permission: coalgov.claim
permit:
description: CoalGov permit commands.
usage: /permit <buy|list>
permission: coalgov.permit
land:
description: CoalGov land commands.
usage: /land <info|create|delete|list>
market:
description: CoalGov resource market.
usage: /market
permission: coalgov.market
diviningrod:
description: Receive a CoalGov divining rod.
usage: /diviningrod
permission: coalgov.diviningrod
police:
description: CoalGov police commands.
usage: /police <fine>
permission: coalgov.police
cgnpc:
description: CoalGov NPC commands.
usage: /cgnpc <zone|create|remove|list|stock|funds|haggle>
permission: coalgov.admin
coalgov:
description: CoalGov admin commands.
usage: /coalgov admin <balance|grant|take|reload|spawn|bypass|rod|treasury|tax>
permission: coalgov.admin
permissions:
coalgov.admin:
description: Allows CoalGov admin commands. Gameplay bypass must be enabled with /coalgov admin bypass on.
default: op
coalgov.coal:
description: Allows coal economy commands.
default: true
coalgov.claim:
description: Allows claim commands.
default: true
coalgov.permit:
description: Allows permit commands.
default: true
coalgov.land.info:
description: Allows land info commands.
default: true
coalgov.market:
description: Allows market trading.
default: true
coalgov.diviningrod:
description: Allows receiving a divining rod.
default: true
coalgov.police:
description: Allows issuing police fines.
default: op