Add newer CoalGov update

This commit is contained in:
CoalGov Deploy
2026-07-11 02:06:53 +00:00
parent dd69ab17a5
commit 963cdd2d9d
39 changed files with 2763 additions and 43 deletions

View File

@@ -1,6 +1,7 @@
package com.librewiki.coalgov;
import com.librewiki.coalgov.command.AdminCommand;
import com.librewiki.coalgov.command.AuctionCommand;
import com.librewiki.coalgov.command.ClaimCommand;
import com.librewiki.coalgov.command.CoalCommand;
import com.librewiki.coalgov.command.DiviningRodCommand;
@@ -9,6 +10,7 @@ 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.command.PreserveCommand;
import com.librewiki.coalgov.command.ShowPropertyLinesCommand;
import com.librewiki.coalgov.listener.BlockBreakListener;
import com.librewiki.coalgov.listener.BlockPlaceListener;
@@ -16,10 +18,13 @@ import com.librewiki.coalgov.listener.AnimalProtectionListener;
import com.librewiki.coalgov.listener.ClaimWandListener;
import com.librewiki.coalgov.listener.DiviningRodListener;
import com.librewiki.coalgov.listener.MarketListener;
import com.librewiki.coalgov.listener.NameTagRecoveryListener;
import com.librewiki.coalgov.listener.NpcListener;
import com.librewiki.coalgov.listener.PlayerJoinListener;
import com.librewiki.coalgov.listener.PropertyInteractListener;
import com.librewiki.coalgov.listener.PreserveProtectionListener;
import com.librewiki.coalgov.listener.SuperFurnaceListener;
import com.librewiki.coalgov.service.AuctionService;
import com.librewiki.coalgov.service.BuildProtectionService;
import com.librewiki.coalgov.service.ClaimMarkerService;
import com.librewiki.coalgov.service.ClaimSelectionService;
@@ -30,23 +35,28 @@ 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.LotteryService;
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.PreserveWorldService;
import com.librewiki.coalgov.service.SpawnGuideService;
import com.librewiki.coalgov.service.SuperFurnaceService;
import com.librewiki.coalgov.service.TaxService;
import com.librewiki.coalgov.service.WebTokenService;
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.LotteryRepository;
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.storage.WebTokenRepository;
import com.librewiki.coalgov.util.CoalMoney;
import com.librewiki.coalgov.util.Messages;
import org.bukkit.entity.Player;
@@ -61,6 +71,7 @@ import java.util.UUID;
public final class CoalGovPlugin extends JavaPlugin {
private Database database;
private EconomyService economyService;
private AuctionService auctionService;
private GovernmentService governmentService;
private ClaimService claimService;
private ClaimMarkerService claimMarkerService;
@@ -72,12 +83,15 @@ public final class CoalGovPlugin extends JavaPlugin {
private NpcService npcService;
private OpenRouterService openRouterService;
private LandService landService;
private LotteryService lotteryService;
private PermitService permitService;
private PreserveWorldService preserveWorldService;
private MiningService miningService;
private DiviningRodService diviningRodService;
private SuperFurnaceService superFurnaceService;
private SpawnGuideService spawnGuideService;
private TaxService taxService;
private WebTokenService webTokenService;
private Messages messages;
private final Set<UUID> adminBypassPlayers = new HashSet<>();
@@ -99,11 +113,14 @@ public final class CoalGovPlugin extends JavaPlugin {
ClaimRepository claimRepository = new ClaimRepository(database);
PermitRepository permitRepository = new PermitRepository(database);
MarketRepository marketRepository = new MarketRepository(database);
LotteryRepository lotteryRepository = new LotteryRepository(database);
GovernmentRepository governmentRepository = new GovernmentRepository(database);
SuperFurnaceRepository superFurnaceRepository = new SuperFurnaceRepository(database);
NpcRepository npcRepository = new NpcRepository(database);
WebTokenRepository webTokenRepository = new WebTokenRepository(database);
economyService = new EconomyService(economyRepository, CoalMoney.fromCoalConfig(getConfig().getDouble("economy.starting_balance", 0.0D)));
auctionService = new AuctionService(this);
governmentService = new GovernmentService(governmentRepository, economyService, getConfig());
landService = new LandService(landRepository, getConfig());
claimService = new ClaimService(claimRepository, landService, economyService, getConfig());
@@ -113,16 +130,20 @@ public final class CoalGovPlugin extends JavaPlugin {
buildProtectionService = new BuildProtectionService(this, claimService);
landAppraisalService = new LandAppraisalService(this, claimService);
marketService = new MarketService(this, marketRepository);
lotteryService = new LotteryService(lotteryRepository, economyService, governmentService, getConfig());
npcService = new NpcService(this, npcRepository);
openRouterService = new OpenRouterService(this);
permitService = new PermitService(permitRepository, landService, economyService, getConfig());
preserveWorldService = new PreserveWorldService(this);
miningService = new MiningService(this, landService, claimService, permitService);
diviningRodService = new DiviningRodService(this);
superFurnaceService = new SuperFurnaceService(this, superFurnaceRepository);
spawnGuideService = new SpawnGuideService(this);
taxService = new TaxService();
webTokenService = new WebTokenService(webTokenRepository, getConfig());
Objects.requireNonNull(getCommand("coal")).setExecutor(new CoalCommand(this));
Objects.requireNonNull(getCommand("auction")).setExecutor(new AuctionCommand(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));
@@ -132,6 +153,7 @@ public final class CoalGovPlugin extends JavaPlugin {
Objects.requireNonNull(getCommand("police")).setExecutor(new PoliceCommand(this));
Objects.requireNonNull(getCommand("cgnpc")).setExecutor(new NpcCommand(this));
Objects.requireNonNull(getCommand("coalgov")).setExecutor(new AdminCommand(this));
Objects.requireNonNull(getCommand("preserve")).setExecutor(new PreserveCommand(this));
getServer().getPluginManager().registerEvents(new BlockBreakListener(this), this);
getServer().getPluginManager().registerEvents(new BlockPlaceListener(this), this);
@@ -140,11 +162,14 @@ public final class CoalGovPlugin extends JavaPlugin {
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 NameTagRecoveryListener(this), this);
getServer().getPluginManager().registerEvents(new NpcListener(this), this);
getServer().getPluginManager().registerEvents(new PlayerJoinListener(this), this);
getServer().getPluginManager().registerEvents(new SuperFurnaceListener(this), this);
getServer().getPluginManager().registerEvents(new PreserveProtectionListener(this), this);
superFurnaceService.registerRecipe();
getServer().getScheduler().runTaskLater(this, () -> claimMarkerService.placeAllLoadedMarkers(), 40L);
getServer().getScheduler().runTaskLater(this, () -> preserveWorldService.loadPreserveWorld(), 60L);
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.");
@@ -152,6 +177,9 @@ public final class CoalGovPlugin extends JavaPlugin {
@Override
public void onDisable() {
if (auctionService != null) {
auctionService.cancelAll();
}
if (database != null) {
database.close();
}
@@ -164,6 +192,9 @@ public final class CoalGovPlugin extends JavaPlugin {
landService.setConfig(getConfig());
claimService.setConfig(getConfig());
permitService.setConfig(getConfig());
lotteryService.setConfig(getConfig());
webTokenService.setConfig(getConfig());
preserveWorldService.setConfig(getConfig());
superFurnaceService.registerRecipe();
}
@@ -183,6 +214,10 @@ public final class CoalGovPlugin extends JavaPlugin {
return economyService;
}
public AuctionService auctionService() {
return auctionService;
}
public GovernmentService governmentService() {
return governmentService;
}
@@ -215,6 +250,10 @@ public final class CoalGovPlugin extends JavaPlugin {
return marketService;
}
public LotteryService lotteryService() {
return lotteryService;
}
public NpcService npcService() {
return npcService;
}
@@ -251,6 +290,14 @@ public final class CoalGovPlugin extends JavaPlugin {
return taxService;
}
public WebTokenService webTokenService() {
return webTokenService;
}
public PreserveWorldService preserveWorldService() {
return preserveWorldService;
}
public Messages messages() {
return messages;
}

View File

@@ -1,15 +1,24 @@
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.service.ClaimService;
import com.librewiki.coalgov.util.CoalMoney;
import org.bukkit.Bukkit;
import org.bukkit.Location;
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.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import java.sql.SQLException;
import java.util.List;
import java.util.Optional;
public final class AdminCommand implements CommandExecutor {
private final CoalGovPlugin plugin;
@@ -21,6 +30,10 @@ public final class AdminCommand implements CommandExecutor {
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (args.length == 0 || !args[0].equalsIgnoreCase("admin")) {
if (args.length == 1 && args[0].equalsIgnoreCase("webtoken")) {
webToken(sender);
return true;
}
usage(sender);
return true;
}
@@ -46,6 +59,14 @@ public final class AdminCommand implements CommandExecutor {
rod(sender, args);
return true;
}
if (args.length >= 2 && args[1].equalsIgnoreCase("furnace")) {
furnace(sender, args);
return true;
}
if (args.length >= 2 && args[1].equalsIgnoreCase("mob")) {
mob(sender, args);
return true;
}
if (args.length >= 2 && args[1].equalsIgnoreCase("treasury")) {
treasury(sender, args);
return true;
@@ -54,6 +75,10 @@ public final class AdminCommand implements CommandExecutor {
tax(sender, args);
return true;
}
if (args.length >= 2 && args[1].equalsIgnoreCase("govtland")) {
govtLand(sender, args);
return true;
}
if (args.length < 3) {
usage(sender);
return true;
@@ -75,7 +100,24 @@ public final class AdminCommand implements CommandExecutor {
}
private void usage(CommandSender sender) {
sender.sendMessage(plugin.messages().text("&eUsage: /coalgov admin <balance|grant|take|reload|spawn|bypass|rod|treasury|tax>"));
sender.sendMessage(plugin.messages().text("&eUsage: /coalgov webtoken"));
sender.sendMessage(plugin.messages().text("&eAdmin: /coalgov admin <balance|grant|take|reload|spawn|bypass|rod|furnace|mob|treasury|tax|govtland>"));
}
private void webToken(CommandSender sender) {
if (!(sender instanceof Player player)) {
sender.sendMessage("Players only.");
return;
}
try {
plugin.economyService().ensurePlayer(player);
var issue = plugin.webTokenService().issue(player);
player.sendMessage(plugin.messages().text("&eYour property management token is &f" + issue.token()
+ "&e. This token expires in &f" + issue.expiresMinutes() + " &eminutes and is single-use."));
} catch (SQLException exception) {
plugin.getLogger().warning("Web token failed: " + exception.getMessage());
player.sendMessage(plugin.messages().text("&cWeb token service unavailable."));
}
}
private void bypass(CommandSender sender, String[] args) {
@@ -131,6 +173,48 @@ public final class AdminCommand implements CommandExecutor {
player.sendMessage(plugin.messages().text("&aDivining rod granted."));
}
private void furnace(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 furnace"));
return;
}
player.getInventory().addItem(plugin.superFurnaceService().item());
player.sendMessage(plugin.messages().text("&aSuper furnace granted."));
}
private void mob(CommandSender sender, String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage("Players only.");
return;
}
if (args.length < 3 || args.length > 4) {
player.sendMessage(plugin.messages().text("&eUsage: /coalgov admin mob <wandering_trader|trader_llama> [count]"));
return;
}
EntityType type = switch (args[2].toLowerCase()) {
case "wandering_trader", "trader" -> EntityType.WANDERING_TRADER;
case "trader_llama", "llama" -> EntityType.TRADER_LLAMA;
default -> null;
};
int count = args.length == 4 ? (int) parseAmount(args[3]) : 1;
if (type == null || count <= 0 || count > 16) {
player.sendMessage(plugin.messages().text("&cInvalid mob type or count."));
return;
}
Location location = player.getLocation();
for (int i = 0; i < count; i++) {
var entity = player.getWorld().spawnEntity(location, type);
if (entity.getType() == EntityType.WANDERING_TRADER) {
entity.getWorld().dropItemNaturally(location, new ItemStack(Material.EMERALD, 1));
}
}
player.sendMessage(plugin.messages().text("&aSpawned &f" + count + " &a" + args[2] + "."));
}
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())));
@@ -179,6 +263,93 @@ public final class AdminCommand implements CommandExecutor {
sender.sendMessage(plugin.messages().text("&eUsage: /coalgov admin tax exempt <player> <on|off|status>"));
}
private void govtLand(CommandSender sender, String[] args) throws SQLException {
if (args.length < 3) {
sender.sendMessage(plugin.messages().text("&eUsage: /coalgov admin govtland <create|sell|info>"));
return;
}
switch (args[2].toLowerCase()) {
case "create" -> govtLandCreate(sender, args);
case "sell" -> govtLandSell(sender, args);
case "info" -> govtLandInfo(sender, args);
default -> sender.sendMessage(plugin.messages().text("&eUsage: /coalgov admin govtland <create|sell|info>"));
}
}
private void govtLandCreate(CommandSender sender, String[] args) throws SQLException {
if (!(sender instanceof Player player)) {
sender.sendMessage("Players only.");
return;
}
if (args.length != 4 && args.length != 5) {
player.sendMessage(plugin.messages().text("&eUsage: /coalgov admin govtland create <homestead|industrial> [radius]"));
return;
}
ClaimService.BuyResult result;
if (args.length == 5) {
int radius = parseInt(args[4]);
result = plugin.claimService().createGovernmentClaim(player.getUniqueId(), player.getLocation(), args[3], radius);
} else {
List<ClaimPoint> points = plugin.claimSelectionService().claimPoints(player);
result = plugin.claimService().createGovernmentPolygon(player.getUniqueId(), player.getWorld().getName(), args[3], points);
}
if (!result.success()) {
player.sendMessage(plugin.messages().text("&c" + result.message()));
return;
}
plugin.claimSelectionService().consume(player);
plugin.claimService().findById(result.id()).ifPresent(claim -> {
plugin.claimMarkerService().placeMarkers(claim);
plugin.claimVisualService().show(player, claim);
});
player.sendMessage(plugin.messages().text("&aGovernment claim #" + result.id() + " created. You were granted conventional claim permissions on it."));
}
private void govtLandSell(CommandSender sender, String[] args) throws SQLException {
if (args.length != 6) {
sender.sendMessage(plugin.messages().text("&eUsage: /coalgov admin govtland sell <id> <player> <price>"));
return;
}
long id = parseLong(args[3]);
long price = CoalMoney.parsePositive(args[5]);
if (id <= 0L || price <= 0L) {
sender.sendMessage(plugin.messages().text("&cInvalid claim id or price."));
return;
}
OfflinePlayer target = Bukkit.getOfflinePlayer(args[4]);
plugin.economyService().ensurePlayer(target);
ClaimService.TransferResult result = plugin.claimService().sellGovernmentClaim(id, target.getUniqueId(), price);
if (!result.success()) {
sender.sendMessage(plugin.messages().text("&c" + result.message()));
return;
}
plugin.claimMarkerService().removeMarkers(result.claim());
plugin.claimService().findById(id).ifPresent(plugin.claimMarkerService()::placeMarkers);
plugin.governmentService().collectMarketRevenue(price);
String targetName = target.getName() == null ? target.getUniqueId().toString() : target.getName();
sender.sendMessage(plugin.messages().text("&aSold government claim #" + id + " to &f" + targetName + " &afor &f" + CoalMoney.format(price) + "&a."));
}
private void govtLandInfo(CommandSender sender, String[] args) throws SQLException {
if (args.length != 4) {
sender.sendMessage(plugin.messages().text("&eUsage: /coalgov admin govtland info <id>"));
return;
}
long id = parseLong(args[3]);
if (id <= 0L) {
sender.sendMessage(plugin.messages().text("&cInvalid claim id."));
return;
}
Optional<Claim> claim = plugin.claimService().findById(id);
if (claim.isEmpty()) {
sender.sendMessage(plugin.messages().text("&cNo claim with that id."));
return;
}
Claim c = claim.get();
sender.sendMessage(plugin.messages().text("&eClaim #" + c.id() + ": &f" + c.claimType() + " owner " + c.ownerUuid()
+ (plugin.claimService().isGovernmentClaim(c) ? " &a(government)" : "")));
}
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>"));
@@ -210,4 +381,28 @@ public final class AdminCommand implements CommandExecutor {
}
}
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;
}
}
private long parseAmount(String raw) {
try {
return Long.parseLong(raw);
} catch (NumberFormatException exception) {
return -1L;
}
}
}

View File

@@ -0,0 +1,112 @@
package com.librewiki.coalgov.command;
import com.librewiki.coalgov.CoalGovPlugin;
import com.librewiki.coalgov.service.AuctionService;
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;
public final class AuctionCommand implements CommandExecutor {
private final CoalGovPlugin plugin;
public AuctionCommand(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);
if (args.length == 0 || args[0].equalsIgnoreCase("status")) {
status(player);
} else if (args[0].equalsIgnoreCase("start")) {
start(player, args);
} else if (args[0].equalsIgnoreCase("bid")) {
bid(player, args);
} else if (args[0].equalsIgnoreCase("cancel")) {
if (!plugin.auctionService().cancel(player)) {
player.sendMessage(plugin.messages().text("&cNo cancellable auction."));
}
} else {
usage(player);
}
} catch (SQLException exception) {
plugin.getLogger().warning("Auction command failed: " + exception.getMessage());
player.sendMessage(plugin.messages().text("&cAuction ledger unavailable."));
}
return true;
}
private void start(Player player, String[] args) throws SQLException {
if (args.length < 4 || args.length > 5) {
player.sendMessage(plugin.messages().text("&eUsage: /auction start <material> <amount> <minBid> [seconds]"));
return;
}
Material material = Material.matchMaterial(args[1]);
int amount = (int) parseLong(args[2]);
long minimumBid = CoalMoney.parsePositive(args[3]);
int seconds = args.length == 5 ? (int) parseLong(args[4]) : plugin.getConfig().getInt("auction.default_seconds", 120);
if (material == null || amount <= 0 || minimumBid <= 0L) {
player.sendMessage(plugin.messages().text("&cInvalid material, amount, or bid."));
return;
}
var result = plugin.auctionService().start(player, material, amount, minimumBid, seconds);
if (!result.success()) {
player.sendMessage(plugin.messages().text("&c" + result.message()));
}
}
private void bid(Player player, String[] args) throws SQLException {
if (args.length != 2) {
player.sendMessage(plugin.messages().text("&eUsage: /auction bid <amount>"));
return;
}
long amount = CoalMoney.parsePositive(args[1]);
if (amount <= 0L) {
player.sendMessage(plugin.messages().text("&cInvalid bid."));
return;
}
var result = plugin.auctionService().bid(player, amount);
if (!result.success()) {
player.sendMessage(plugin.messages().text("&c" + result.message()));
}
}
private void status(Player player) {
AuctionService.Status status = plugin.auctionService().status();
if (status == null) {
player.sendMessage(plugin.messages().text("&7No auction is running."));
return;
}
String highBid = status.highBid() > 0L
? CoalMoney.format(status.highBid()) + " by " + status.highBidderName()
: "none";
player.sendMessage(plugin.messages().text("&eAuction: &f" + status.amount() + " "
+ status.material().name().toLowerCase().replace('_', ' ')
+ " &7seller &f" + status.sellerName()
+ " &7minimum &f" + CoalMoney.format(status.minimumBid())
+ " &7high bid &f" + highBid
+ " &7time &f" + status.remainingSeconds() + "s"));
}
private void usage(Player player) {
player.sendMessage(plugin.messages().text("&eUsage: /auction <start|bid|status|cancel>"));
}
private long parseLong(String raw) {
try {
return Long.parseLong(raw);
} catch (NumberFormatException exception) {
return -1L;
}
}
}

View File

@@ -213,12 +213,12 @@ public final class ClaimCommand implements CommandExecutor {
}
private void trust(Player player, String[] args) throws SQLException {
if (args.length != 5) {
player.sendMessage(plugin.messages().text("&eUsage: /claim trust <id> <player> <build|interact|container|animal|manage|all>"));
if (args.length != 4) {
player.sendMessage(plugin.messages().text("&eUsage: /claim trust <id> <player> <build|interact|container|animal|manage|all|any>"));
return;
}
long id = parseLong(args[1]);
List<ClaimPermission> permissions = ClaimPermission.grantSet(args[4]);
List<ClaimPermission> permissions = ClaimPermission.grantSet(args[3]);
if (id <= 0L || permissions.isEmpty()) {
player.sendMessage(plugin.messages().text("&cInvalid claim id or permission."));
return;
@@ -233,12 +233,12 @@ public final class ClaimCommand implements CommandExecutor {
return;
}
}
player.sendMessage(plugin.messages().text("&aGranted &f" + args[4].toLowerCase() + " &aaccess on claim #" + id + " to &f" + targetName(target) + "&a."));
player.sendMessage(plugin.messages().text("&aGranted &f" + args[3].toLowerCase() + " &aaccess on claim #" + id + " to &f" + targetName(target) + "&a."));
}
private void untrust(Player player, String[] args) throws SQLException {
if (args.length != 5) {
player.sendMessage(plugin.messages().text("&eUsage: /claim untrust <id> <player> <build|interact|container|animal|manage|all>"));
if (args.length != 4) {
player.sendMessage(plugin.messages().text("&eUsage: /claim untrust <id> <player> <build|interact|container|animal|manage|all|any>"));
return;
}
long id = parseLong(args[1]);
@@ -247,7 +247,7 @@ public final class ClaimCommand implements CommandExecutor {
return;
}
OfflinePlayer target = Bukkit.getOfflinePlayer(args[2]);
if (args[4].equalsIgnoreCase("all")) {
if (args[3].equalsIgnoreCase("all") || args[3].equalsIgnoreCase("any")) {
ClaimService.PermissionResult result = plugin.claimService()
.revokeAllPermissions(player.getUniqueId(), id, target.getUniqueId());
if (!result.success()) {
@@ -257,7 +257,7 @@ public final class ClaimCommand implements CommandExecutor {
player.sendMessage(plugin.messages().text("&aRemoved all claim access for &f" + targetName(target) + "&a."));
return;
}
Optional<ClaimPermission> permission = ClaimPermission.parse(args[4]);
Optional<ClaimPermission> permission = ClaimPermission.parse(args[3]);
if (permission.isEmpty()) {
player.sendMessage(plugin.messages().text("&cInvalid permission."));
return;

View File

@@ -28,7 +28,7 @@ public final class CoalCommand implements CommandExecutor {
return true;
}
if (args.length == 0) {
player.sendMessage(plugin.messages().text("&eUsage: /coal <balance|deposit|withdraw|pay|fines>"));
player.sendMessage(plugin.messages().text("&eUsage: /coal <balance|deposit|withdraw|pay|fines|networth|lottery>"));
return true;
}
try {
@@ -39,7 +39,9 @@ public final class CoalCommand implements CommandExecutor {
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>"));
case "networth" -> networth(player);
case "lottery" -> lottery(player, args);
default -> player.sendMessage(plugin.messages().text("&eUsage: /coal <balance|deposit|withdraw|pay|fines|networth|lottery>"));
}
} catch (SQLException exception) {
plugin.getLogger().warning("Coal command failed: " + exception.getMessage());
@@ -52,6 +54,29 @@ public final class CoalCommand implements CommandExecutor {
player.sendMessage(plugin.messages().text("&eBalance: &f" + CoalMoney.format(plugin.economyService().balance(player.getUniqueId()))));
}
private void networth(Player player) throws SQLException {
long balance = plugin.economyService().balance(player.getUniqueId());
long inventory = 0L;
for (ItemStack item : player.getInventory().getStorageContents()) {
if (item == null || item.getType().isAir()) {
continue;
}
Double price = plugin.marketService().basePrices().get(item.getType());
if (price != null) {
inventory += CoalMoney.fromCoal(price * item.getAmount());
}
}
long claims = plugin.claimService().list(player.getUniqueId()).stream()
.mapToLong(plugin.claimService()::claimPurchaseCost)
.filter(value -> value > 0L)
.sum();
long total = balance + inventory + claims;
player.sendMessage(plugin.messages().text("&eNet worth: &f" + CoalMoney.format(total)));
player.sendMessage(plugin.messages().text("&7Balance: &f" + CoalMoney.format(balance)
+ " &7Inventory: &f" + CoalMoney.format(inventory)
+ " &7Claims: &f" + CoalMoney.format(claims)));
}
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);
@@ -177,6 +202,53 @@ public final class CoalCommand implements CommandExecutor {
player.sendMessage(plugin.messages().text("&eUsage: /coal fines [list|pay <id|all>]"));
}
private void lottery(Player player, String[] args) throws SQLException {
if (args.length == 1 || (args.length == 2 && args[1].equalsIgnoreCase("status"))) {
var status = plugin.lotteryService().status();
player.sendMessage(plugin.messages().text("&eLottery round #" + status.round()
+ ": &f" + status.tickets() + " &etickets from &f" + status.players()
+ " &eplayers. Pot: &f" + CoalMoney.format(status.pot())));
return;
}
if (args.length == 2 && args[1].equalsIgnoreCase("buy")) {
buyLottery(player, 1);
return;
}
if (args.length == 3 && args[1].equalsIgnoreCase("buy")) {
int tickets = (int) parsePositive(args[2]);
buyLottery(player, tickets);
return;
}
if (args.length == 2 && args[1].equalsIgnoreCase("draw")) {
if (!player.hasPermission("coalgov.admin")) {
player.sendMessage(plugin.messages().text("&cOnly CoalGov admins can draw the lottery."));
return;
}
var result = plugin.lotteryService().draw();
if (!result.success()) {
player.sendMessage(plugin.messages().text("&c" + result.message()));
return;
}
Bukkit.broadcastMessage(plugin.messages().text("&6Lottery round #" + result.round()
+ " won by &f" + result.winnerName()
+ " &6for &f" + CoalMoney.format(result.pot())
+ " &6from &f" + result.tickets() + " &6tickets."));
return;
}
player.sendMessage(plugin.messages().text("&eUsage: /coal lottery <status|buy [tickets]|draw>"));
}
private void buyLottery(Player player, int tickets) throws SQLException {
var result = plugin.lotteryService().buy(player, tickets);
if (!result.success()) {
player.sendMessage(plugin.messages().text("&c" + result.message()));
return;
}
player.sendMessage(plugin.messages().text("&aBought &f" + result.tickets()
+ " &alottery ticket(s) for &f" + CoalMoney.format(result.cost())
+ " &ain round #" + result.round() + "."));
}
private boolean canFitCoal(Player player, long amount) {
if (amount > Integer.MAX_VALUE) {
return false;

View File

@@ -43,6 +43,14 @@ public final class NpcCommand implements CommandExecutor {
stock(sender, args);
} else if (args.length >= 1 && args[0].equalsIgnoreCase("funds")) {
funds(sender, args);
} else if (args.length >= 1 && args[0].equalsIgnoreCase("role")) {
role(sender, args);
} else if (args.length >= 1 && args[0].equalsIgnoreCase("home")) {
home(sender, args);
} else if (args.length >= 1 && args[0].equalsIgnoreCase("work")) {
work(sender, args);
} else if (args.length >= 1 && args[0].equalsIgnoreCase("needs")) {
needs(sender, args);
} else if (args.length >= 1 && args[0].equalsIgnoreCase("haggle")) {
haggle(sender, args);
} else {
@@ -117,7 +125,9 @@ public final class NpcCommand implements CommandExecutor {
}
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()));
String role = npc.role() == null || npc.role().isBlank() ? "auto" : npc.role();
sender.sendMessage(plugin.messages().raw("&7#" + npc.id() + " &f" + npc.name() + " " + npc.npcType()
+ " role " + role + " hunger " + npc.hunger() + "/20 citizens " + npc.citizensId()));
}
}
@@ -152,6 +162,73 @@ public final class NpcCommand implements CommandExecutor {
sender.sendMessage(plugin.messages().text("&aNPC funded."));
}
private void role(CommandSender sender, String[] args) throws SQLException {
if (args.length != 3) {
sender.sendMessage(plugin.messages().text("&eUsage: /cgnpc role <id> <role>"));
return;
}
long id = parseLong(args[1]);
if (!plugin.npcService().setRole(id, args[2])) {
sender.sendMessage(plugin.messages().text("&cNo NPC with that id."));
return;
}
sender.sendMessage(plugin.messages().text("&aNPC role set to &f" + args[2] + "&a."));
}
private void home(CommandSender sender, String[] args) throws SQLException {
if (!(sender instanceof Player player)) {
sender.sendMessage("Players only.");
return;
}
if (args.length != 2) {
player.sendMessage(plugin.messages().text("&eUsage: /cgnpc home <id>"));
return;
}
if (!plugin.npcService().setHome(parseLong(args[1]), player.getLocation())) {
player.sendMessage(plugin.messages().text("&cNo NPC with that id."));
return;
}
player.sendMessage(plugin.messages().text("&aNPC home set to your location."));
}
private void work(CommandSender sender, String[] args) throws SQLException {
if (!(sender instanceof Player player)) {
sender.sendMessage("Players only.");
return;
}
if (args.length != 2) {
player.sendMessage(plugin.messages().text("&eUsage: /cgnpc work <id>"));
return;
}
if (!plugin.npcService().setWork(parseLong(args[1]), player.getLocation())) {
player.sendMessage(plugin.messages().text("&cNo NPC with that id."));
return;
}
player.sendMessage(plugin.messages().text("&aNPC work location set to your location."));
}
private void needs(CommandSender sender, String[] args) throws SQLException {
if (args.length != 2) {
sender.sendMessage(plugin.messages().text("&eUsage: /cgnpc needs <id>"));
return;
}
Optional<CoalNpc> npc = plugin.npcService().findNpc(parseLong(args[1]));
if (npc.isEmpty()) {
sender.sendMessage(plugin.messages().text("&cNo NPC with that id."));
return;
}
CoalNpc coalNpc = npc.get();
sender.sendMessage(plugin.messages().text("&e" + coalNpc.name() + " needs: &f"
+ coalNpc.hunger() + "/20 hunger &7Role: &f"
+ (coalNpc.role() == null || coalNpc.role().isBlank() ? "auto" : coalNpc.role())));
sender.sendMessage(plugin.messages().text("&7Home: &f" + coalNpc.world() + " "
+ Math.round(coalNpc.x()) + " " + Math.round(coalNpc.y()) + " " + Math.round(coalNpc.z())));
if (coalNpc.workWorld() != null) {
sender.sendMessage(plugin.messages().text("&7Work: &f" + coalNpc.workWorld() + " "
+ Math.round(coalNpc.workX()) + " " + Math.round(coalNpc.workY()) + " " + Math.round(coalNpc.workZ())));
}
}
private void haggle(CommandSender sender, String[] args) throws SQLException {
if (!(sender instanceof Player player)) {
sender.sendMessage("Players only.");
@@ -183,13 +260,15 @@ public final class NpcCommand implements CommandExecutor {
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;
long tradePrice = acceptedTradePrice(quote, offer, finalPrice);
boolean aiAccepted = ai.map(OpenRouterService.HaggleAiResult::accept).orElse(false) && offer == finalPrice;
boolean accepted = quote.accepted() || aiAccepted;
String line = ai.map(OpenRouterService.HaggleAiResult::message)
.orElse(accepted ? "Deal." : "I can do " + CoalMoney.format(finalPrice) + ".");
.orElse(accepted ? "Deal at " + CoalMoney.format(tradePrice) + "." : "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."));
if (plugin.npcService().completeTrade(player, npc.get(), quote, tradePrice)) {
player.sendMessage(plugin.messages().text("&aTrade complete for &f" + CoalMoney.format(tradePrice) + "&a."));
} else {
player.sendMessage(plugin.messages().text("&cTrade failed. Check balance, stock, and inventory."));
}
@@ -202,8 +281,18 @@ public final class NpcCommand implements CommandExecutor {
}));
}
private long acceptedTradePrice(NpcService.HaggleQuote quote, long offer, long counter) {
if (!quote.accepted()) {
return counter;
}
if (quote.mode().equals("buy")) {
return Math.min(offer, quote.ceiling());
}
return Math.min(offer, quote.ceiling());
}
private void usage(CommandSender sender) {
sender.sendMessage(plugin.messages().text("&eUsage: /cgnpc <zone|create|remove|list|stock|funds|haggle>"));
sender.sendMessage(plugin.messages().text("&eUsage: /cgnpc <zone|create|remove|list|stock|funds|role|home|work|needs|haggle>"));
}
private long parseLong(String raw) {

View File

@@ -0,0 +1,43 @@
package com.librewiki.coalgov.command;
import com.librewiki.coalgov.CoalGovPlugin;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
public final class PreserveCommand implements CommandExecutor {
private final CoalGovPlugin plugin;
public PreserveCommand(CoalGovPlugin plugin) {
this.plugin = plugin;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage("Only players can use /preserve.");
return true;
}
if (args.length > 0 && args[0].equalsIgnoreCase("return")) {
World mainWorld = plugin.getServer().getWorld("world");
if (mainWorld == null) {
player.sendMessage(plugin.messages().text("&cThe main world is not loaded."));
return true;
}
player.teleport(mainWorld.getSpawnLocation());
player.sendMessage(plugin.messages().text("&aReturned to CoalGov Basin."));
return true;
}
Location location = plugin.preserveWorldService().teleportLocation();
if (location == null) {
player.sendMessage(plugin.messages().text("&cThe wildlife preserve is not available yet."));
return true;
}
player.teleport(location);
player.sendMessage(plugin.messages().text("&aEntering the wildlife preserve. Building and mining are not allowed here."));
return true;
}
}

View File

@@ -0,0 +1,37 @@
package com.librewiki.coalgov.listener;
import com.librewiki.coalgov.CoalGovPlugin;
import org.bukkit.Material;
import org.bukkit.entity.LivingEntity;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDeathEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import java.util.List;
public final class NameTagRecoveryListener implements Listener {
private final CoalGovPlugin plugin;
public NameTagRecoveryListener(CoalGovPlugin plugin) {
this.plugin = plugin;
}
@EventHandler(ignoreCancelled = true)
public void onEntityDeath(EntityDeathEvent event) {
if (!plugin.getConfig().getBoolean("name_tags.recover_named_entity_tags", true)) {
return;
}
LivingEntity entity = event.getEntity();
if (entity.customName() == null || !entity.isCustomNameVisible()) {
return;
}
ItemStack tag = new ItemStack(Material.NAME_TAG);
ItemMeta meta = tag.getItemMeta();
meta.setDisplayName(plugin.messages().raw("&fRecovered Name Tag"));
meta.setLore(List.of(plugin.messages().raw("&7Recovered from a named entity.")));
tag.setItemMeta(meta);
event.getDrops().add(tag);
}
}

View File

@@ -0,0 +1,141 @@
package com.librewiki.coalgov.listener;
import com.librewiki.coalgov.CoalGovPlugin;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockBurnEvent;
import org.bukkit.event.block.BlockExplodeEvent;
import org.bukkit.event.block.BlockFadeEvent;
import org.bukkit.event.block.BlockIgniteEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.block.EntityBlockFormEvent;
import org.bukkit.event.entity.EntityChangeBlockEvent;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.EntityExplodeEvent;
import org.bukkit.event.player.PlayerBucketEmptyEvent;
import org.bukkit.event.player.PlayerBucketFillEvent;
import org.bukkit.event.player.PlayerInteractEvent;
public final class PreserveProtectionListener implements Listener {
private final CoalGovPlugin plugin;
public PreserveProtectionListener(CoalGovPlugin plugin) {
this.plugin = plugin;
}
@EventHandler(ignoreCancelled = true)
public void onBlockBreak(BlockBreakEvent event) {
if (denyPlayer(event.getPlayer(), event.getBlock().getLocation())) {
event.setCancelled(true);
event.getPlayer().sendMessage(plugin.messages().text("&cThe wildlife preserve is protected. Use /preserve return to leave."));
}
}
@EventHandler(ignoreCancelled = true)
public void onBlockPlace(BlockPlaceEvent event) {
if (denyPlayer(event.getPlayer(), event.getBlockPlaced().getLocation())) {
event.setCancelled(true);
event.getPlayer().sendMessage(plugin.messages().text("&cBuilding is not allowed in the wildlife preserve."));
}
}
@EventHandler(ignoreCancelled = true)
public void onBucketEmpty(PlayerBucketEmptyEvent event) {
if (denyPlayer(event.getPlayer(), event.getBlockClicked().getLocation())) {
event.setCancelled(true);
}
}
@EventHandler(ignoreCancelled = true)
public void onBucketFill(PlayerBucketFillEvent event) {
if (denyPlayer(event.getPlayer(), event.getBlockClicked().getLocation())) {
event.setCancelled(true);
}
}
@EventHandler(ignoreCancelled = true)
public void onInteract(PlayerInteractEvent event) {
if (event.getClickedBlock() != null && denyPlayer(event.getPlayer(), event.getClickedBlock().getLocation())) {
event.setCancelled(true);
}
}
@EventHandler(ignoreCancelled = true)
public void onEntityDamage(EntityDamageByEntityEvent event) {
Player player = attackingPlayer(event.getDamager());
if (player != null && denyPlayer(player, event.getEntity().getLocation())) {
event.setCancelled(true);
player.sendMessage(plugin.messages().text("&cWildlife is protected in the preserve."));
}
}
@EventHandler(ignoreCancelled = true)
public void onEntityChangeBlock(EntityChangeBlockEvent event) {
if (isPreserve(event.getBlock().getLocation())) {
event.setCancelled(true);
}
}
@EventHandler(ignoreCancelled = true)
public void onEntityBlockForm(EntityBlockFormEvent event) {
if (isPreserve(event.getBlock().getLocation())) {
event.setCancelled(true);
}
}
@EventHandler(ignoreCancelled = true)
public void onBlockFade(BlockFadeEvent event) {
if (isPreserve(event.getBlock().getLocation())) {
event.setCancelled(true);
}
}
@EventHandler(ignoreCancelled = true)
public void onBlockBurn(BlockBurnEvent event) {
if (isPreserve(event.getBlock().getLocation())) {
event.setCancelled(true);
}
}
@EventHandler(ignoreCancelled = true)
public void onBlockIgnite(BlockIgniteEvent event) {
if (isPreserve(event.getBlock().getLocation())) {
event.setCancelled(true);
}
}
@EventHandler(ignoreCancelled = true)
public void onEntityExplode(EntityExplodeEvent event) {
if (isPreserve(event.getLocation())) {
event.blockList().clear();
}
}
@EventHandler(ignoreCancelled = true)
public void onBlockExplode(BlockExplodeEvent event) {
if (isPreserve(event.getBlock().getLocation())) {
event.blockList().clear();
}
}
private boolean denyPlayer(Player player, Location location) {
return isPreserve(location) && !plugin.preserveWorldService().canBypass(player);
}
private boolean isPreserve(Location location) {
return plugin.preserveWorldService().isPreserveWorld(location);
}
private Player attackingPlayer(org.bukkit.entity.Entity damager) {
if (damager instanceof Player player) {
return player;
}
if (damager instanceof org.bukkit.entity.Projectile projectile && projectile.getShooter() instanceof Player player) {
return player;
}
return null;
}
}

View File

@@ -2,12 +2,16 @@ package com.librewiki.coalgov.listener;
import com.librewiki.coalgov.CoalGovPlugin;
import org.bukkit.Material;
import org.bukkit.block.Furnace;
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.FurnaceSmeltEvent;
import org.bukkit.event.inventory.FurnaceStartSmeltEvent;
import org.bukkit.inventory.FurnaceInventory;
import org.bukkit.inventory.ItemStack;
import java.sql.SQLException;
@@ -71,4 +75,28 @@ public final class SuperFurnaceListener implements Listener {
plugin.getLogger().warning("Super furnace burn check failed: " + exception.getMessage());
}
}
@EventHandler(ignoreCancelled = true)
public void onFurnaceSmelt(FurnaceSmeltEvent event) {
try {
if (!plugin.superFurnaceService().syntheticDiamondsEnabled()
|| !plugin.superFurnaceService().isSuperFurnace(event.getBlock())
|| event.getSource().getType() != Material.COAL) {
return;
}
int coalInput = plugin.superFurnaceService().syntheticDiamondCoalInput();
if (event.getSource().getAmount() < coalInput || !(event.getBlock().getState() instanceof Furnace furnace)) {
return;
}
FurnaceInventory inventory = furnace.getInventory();
ItemStack fuel = inventory.getFuel();
if (fuel == null || fuel.getType() != Material.COAL) {
return;
}
event.setResult(new ItemStack(Material.DIAMOND));
event.getSource().setAmount(event.getSource().getAmount() - coalInput + 1);
} catch (SQLException exception) {
plugin.getLogger().warning("Synthetic diamond smelt check failed: " + exception.getMessage());
}
}
}

View File

@@ -18,7 +18,7 @@ public enum ClaimPermission {
}
public static List<ClaimPermission> grantSet(String raw) {
if (raw.equalsIgnoreCase("all")) {
if (raw.equalsIgnoreCase("all") || raw.equalsIgnoreCase("any")) {
return List.of(values());
}
return parse(raw).map(List::of).orElse(List.of());

View File

@@ -12,8 +12,30 @@ public record CoalNpc(
String world,
double x,
double y,
double z
double z,
String role,
String workWorld,
Double workX,
Double workY,
Double workZ,
int hunger,
long lastFoodAt
) {
public CoalNpc(
long id,
int citizensId,
UUID accountUuid,
String npcType,
String name,
long zoneId,
String world,
double x,
double y,
double z
) {
this(id, citizensId, accountUuid, npcType, name, zoneId, world, x, y, z, "", null, null, null, null, 20, 0L);
}
public boolean trader() {
return npcType.equalsIgnoreCase("TRADER");
}

View File

@@ -0,0 +1,208 @@
package com.librewiki.coalgov.service;
import com.librewiki.coalgov.CoalGovPlugin;
import com.librewiki.coalgov.util.CoalMoney;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import java.sql.SQLException;
import java.util.UUID;
public final class AuctionService {
private final CoalGovPlugin plugin;
private Auction active;
private int taskId = -1;
public AuctionService(CoalGovPlugin plugin) {
this.plugin = plugin;
}
public StartResult start(Player seller, Material material, int amount, long minimumBid, int seconds) throws SQLException {
if (active != null) {
return StartResult.fail("Another auction is already running.");
}
if (amount <= 0 || minimumBid <= 0L || seconds < 15 || seconds > 600) {
return StartResult.fail("Use a positive amount and bid. Duration must be 15-600 seconds.");
}
if (!removeItems(seller, material, amount)) {
return StartResult.fail("You do not have enough " + readable(material) + ".");
}
active = new Auction(seller.getUniqueId(), seller.getName(), new ItemStack(material, amount),
minimumBid, System.currentTimeMillis() + seconds * 1000L, null, "", 0L);
taskId = Bukkit.getScheduler().runTaskLater(plugin, this::finish, seconds * 20L).getTaskId();
Bukkit.broadcastMessage(plugin.messages().text("&6Auction started: &f" + amount + " " + readable(material)
+ " &6by &f" + seller.getName() + " &6minimum &f" + CoalMoney.format(minimumBid)
+ "&6. Bid with &f/auction bid <amount>&6."));
return StartResult.ok();
}
public BidResult bid(Player bidder, long amount) throws SQLException {
if (active == null) {
return BidResult.fail("No auction is running.");
}
if (active.sellerUuid().equals(bidder.getUniqueId())) {
return BidResult.fail("You cannot bid on your own auction.");
}
long minimum = active.highBid() > 0L
? active.highBid() + bidIncrement()
: active.minimumBid();
if (amount < minimum) {
return BidResult.fail("Bid must be at least " + CoalMoney.format(minimum) + ".");
}
plugin.economyService().ensurePlayer(bidder);
if (!plugin.economyService().charge(bidder.getUniqueId(), amount, "auction_bid_escrow")) {
return BidResult.fail("Balance too low.");
}
if (active.highBidderUuid() != null) {
plugin.economyService().credit(active.highBidderUuid(), active.highBid(), "auction_bid_refund");
}
active = new Auction(active.sellerUuid(), active.sellerName(), active.item(), active.minimumBid(),
active.endsAt(), bidder.getUniqueId(), bidder.getName(), amount);
Bukkit.broadcastMessage(plugin.messages().text("&6Auction bid: &f" + bidder.getName() + " &6bid &f"
+ CoalMoney.format(amount) + " &6on &f" + active.item().getAmount() + " " + readable(active.item().getType()) + "&6."));
return BidResult.ok();
}
public Status status() {
if (active == null) {
return null;
}
long remainingSeconds = Math.max(0L, (active.endsAt() - System.currentTimeMillis()) / 1000L);
return new Status(active.sellerName(), active.item().getType(), active.item().getAmount(),
active.minimumBid(), active.highBidderName(), active.highBid(), remainingSeconds);
}
public boolean cancel(Player actor) throws SQLException {
if (active == null || (!active.sellerUuid().equals(actor.getUniqueId()) && !actor.hasPermission("coalgov.admin"))) {
return false;
}
Auction cancelled = active;
clearTask();
active = null;
if (cancelled.highBidderUuid() != null) {
plugin.economyService().credit(cancelled.highBidderUuid(), cancelled.highBid(), "auction_cancel_refund");
}
giveOrDrop(cancelled.sellerUuid(), cancelled.item());
Bukkit.broadcastMessage(plugin.messages().text("&cAuction cancelled by " + actor.getName() + "."));
return true;
}
public void cancelAll() {
if (active == null) {
return;
}
try {
Auction cancelled = active;
active = null;
clearTask();
if (cancelled.highBidderUuid() != null) {
plugin.economyService().credit(cancelled.highBidderUuid(), cancelled.highBid(), "auction_shutdown_refund");
}
giveOrDrop(cancelled.sellerUuid(), cancelled.item());
} catch (SQLException exception) {
plugin.getLogger().warning("Auction shutdown refund failed: " + exception.getMessage());
}
}
private void finish() {
if (active == null) {
return;
}
Auction ended = active;
active = null;
taskId = -1;
try {
if (ended.highBidderUuid() == null) {
giveOrDrop(ended.sellerUuid(), ended.item());
Bukkit.broadcastMessage(plugin.messages().text("&7Auction ended with no bids: &f"
+ ended.item().getAmount() + " " + readable(ended.item().getType()) + "&7."));
return;
}
plugin.economyService().credit(ended.sellerUuid(), ended.highBid(), "auction_sale");
giveOrDrop(ended.highBidderUuid(), ended.item());
Bukkit.broadcastMessage(plugin.messages().text("&6Auction won by &f" + ended.highBidderName()
+ " &6for &f" + CoalMoney.format(ended.highBid()) + "&6."));
} catch (SQLException exception) {
plugin.getLogger().warning("Auction finish failed: " + exception.getMessage());
}
}
private long bidIncrement() {
return CoalMoney.fromCoalConfig(plugin.getConfig().getDouble("auction.min_increment", 0.25D));
}
private void clearTask() {
if (taskId >= 0) {
Bukkit.getScheduler().cancelTask(taskId);
taskId = -1;
}
}
private boolean removeItems(Player player, Material material, int amount) {
int found = 0;
for (ItemStack item : player.getInventory().getStorageContents()) {
if (item != null && item.getType() == material) {
found += item.getAmount();
}
}
if (found < amount) {
return false;
}
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 true;
}
}
return true;
}
private void giveOrDrop(UUID uuid, ItemStack item) {
Player player = Bukkit.getPlayer(uuid);
if (player == null) {
return;
}
player.getInventory().addItem(item.clone()).values()
.forEach(leftover -> player.getWorld().dropItemNaturally(player.getLocation(), leftover));
}
private String readable(Material material) {
return material.name().toLowerCase().replace('_', ' ');
}
private record Auction(UUID sellerUuid, String sellerName, ItemStack item, long minimumBid,
long endsAt, UUID highBidderUuid, String highBidderName, long highBid) {
}
public record StartResult(boolean success, String message) {
public static StartResult fail(String message) {
return new StartResult(false, message);
}
public static StartResult ok() {
return new StartResult(true, "");
}
}
public record BidResult(boolean success, String message) {
public static BidResult fail(String message) {
return new BidResult(false, message);
}
public static BidResult ok() {
return new BidResult(true, "");
}
}
public record Status(String sellerName, Material material, int amount, long minimumBid,
String highBidderName, long highBid, long remainingSeconds) {
}
}

View File

@@ -77,6 +77,9 @@ public final class ClaimMarkerService {
}
private void placeMarker(World world, int x, int z) {
if (!world.isChunkLoaded(x >> 4, z >> 4)) {
return;
}
int y = world.getHighestBlockYAt(x, z);
Block highest = world.getBlockAt(x, y, z);
if (highest.getType() == MARKER_MATERIAL) {
@@ -86,6 +89,9 @@ public final class ClaimMarkerService {
}
private void removeMarkerColumn(World world, int x, int z) {
if (!world.isChunkLoaded(x >> 4, z >> 4)) {
return;
}
for (int y = world.getMinHeight(); y < world.getMaxHeight(); y++) {
Block block = world.getBlockAt(x, y, z);
if (block.getType() == MARKER_MATERIAL) {

View File

@@ -10,12 +10,15 @@ import com.librewiki.coalgov.util.Cuboid2D;
import org.bukkit.Location;
import org.bukkit.configuration.file.FileConfiguration;
import java.nio.charset.StandardCharsets;
import java.sql.SQLException;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public final class ClaimService {
private static final UUID GOVERNMENT_OWNER_UUID = UUID.nameUUIDFromBytes("CoalGov Ministry".getBytes(StandardCharsets.UTF_8));
private final ClaimRepository repository;
private final LandService landService;
private final EconomyService economyService;
@@ -106,6 +109,85 @@ public final class ClaimService {
return repository.findOwnedById(owner, id);
}
public Optional<Claim> findById(long id) throws SQLException {
return repository.findById(id);
}
public UUID governmentOwnerUuid() {
return GOVERNMENT_OWNER_UUID;
}
public boolean isGovernmentClaim(Claim claim) {
return claim.ownerUuid().equals(GOVERNMENT_OWNER_UUID);
}
public BuyResult createGovernmentClaim(UUID adminUuid, 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
);
return createGovernmentClaim(adminUuid, area, type, List.of());
}
public BuyResult createGovernmentPolygon(UUID adminUuid, 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.");
}
return createGovernmentClaim(adminUuid, area, type, vertices);
}
private BuyResult createGovernmentClaim(UUID adminUuid, Cuboid2D area, String type, List<ClaimPoint> vertices) throws SQLException {
if (repository.overlaps(area)) {
return BuyResult.fail("That claim overlaps an existing claim.");
}
long id = repository.create(GOVERNMENT_OWNER_UUID, area, type.toUpperCase(), null, 0L, vertices);
for (ClaimPermission permission : ClaimPermission.values()) {
repository.grantPermission(id, adminUuid, permission);
}
return BuyResult.success(id, 0L);
}
public TransferResult sellGovernmentClaim(long id, UUID newOwner, long price) throws SQLException {
if (price < 0L) {
return TransferResult.fail("Price cannot be negative.");
}
Optional<Claim> claim = repository.findById(id);
if (claim.isEmpty() || !isGovernmentClaim(claim.get())) {
return TransferResult.fail("No government-owned claim with that id.");
}
if (price > 0L && !economyService.charge(newOwner, price, "government_land_purchase")) {
return TransferResult.fail("Buyer needs " + CoalMoney.format(price) + " for that claim.");
}
if (!repository.transferGovernmentOwned(GOVERNMENT_OWNER_UUID, id, newOwner, price)) {
if (price > 0L) {
economyService.credit(newOwner, price, "government_land_purchase_refund");
}
return TransferResult.fail("Could not transfer that government claim.");
}
return TransferResult.success(claim.get());
}
public SellResult sell(UUID owner, long id, long refund) throws SQLException {
Optional<Claim> claim = repository.findOwnedById(owner, id);
if (claim.isEmpty()) {

View File

@@ -0,0 +1,117 @@
package com.librewiki.coalgov.service;
import com.librewiki.coalgov.storage.LotteryRepository;
import com.librewiki.coalgov.storage.LotteryRepository.TicketHolder;
import com.librewiki.coalgov.util.CoalMoney;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.entity.Player;
import java.sql.SQLException;
import java.util.List;
import java.util.Random;
public final class LotteryService {
private final LotteryRepository repository;
private final EconomyService economyService;
private final GovernmentService governmentService;
private final Random random = new Random();
private FileConfiguration config;
public LotteryService(LotteryRepository repository, EconomyService economyService, GovernmentService governmentService, FileConfiguration config) {
this.repository = repository;
this.economyService = economyService;
this.governmentService = governmentService;
this.config = config;
}
public void setConfig(FileConfiguration config) {
this.config = config;
}
public BuyResult buy(Player player, int tickets) throws SQLException {
if (tickets <= 0 || tickets > maxTicketsPerBuy()) {
return BuyResult.fail("Ticket count must be between 1 and " + maxTicketsPerBuy() + ".");
}
long cost = ticketCost() * tickets;
if (!economyService.charge(player.getUniqueId(), cost, "lottery_ticket")) {
return BuyResult.fail("You need " + CoalMoney.format(cost) + ".");
}
int round = repository.activeRound();
repository.addTickets(round, player.getUniqueId(), player.getName(), tickets);
return BuyResult.ok(round, tickets, cost);
}
public Status status() throws SQLException {
int round = repository.activeRound();
List<TicketHolder> holders = repository.tickets(round);
int tickets = holders.stream().mapToInt(TicketHolder::tickets).sum();
return new Status(round, holders.size(), tickets, tickets * ticketCost());
}
public DrawResult draw() throws SQLException {
int round = repository.activeRound();
List<TicketHolder> holders = repository.tickets(round);
int totalTickets = holders.stream().mapToInt(TicketHolder::tickets).sum();
if (totalTickets <= 0) {
return DrawResult.fail("No tickets have been sold for this round.");
}
int winningTicket = random.nextInt(totalTickets);
TicketHolder winner = null;
int cursor = 0;
for (TicketHolder holder : holders) {
cursor += holder.tickets();
if (winningTicket < cursor) {
winner = holder;
break;
}
}
if (winner == null) {
return DrawResult.fail("Lottery draw failed.");
}
long gross = totalTickets * ticketCost();
long pot = Math.max(0L, Math.round(gross * payoutPercent() / 100.0D));
if (gross > 0L) {
pot = Math.min(pot, Math.max(0L, gross - 1L));
}
economyService.credit(winner.uuid(), pot, "lottery_win");
governmentService.collectTax(Math.max(0L, gross - pot));
repository.markDrawn(round, winner.uuid(), pot);
repository.activeRound();
return DrawResult.ok(round, winner.name(), pot, totalTickets);
}
private long ticketCost() {
return CoalMoney.fromCoalConfig(config.getDouble("lottery.ticket_cost", 1.0D));
}
private int maxTicketsPerBuy() {
return Math.max(1, config.getInt("lottery.max_tickets_per_buy", 64));
}
private double payoutPercent() {
return Math.max(0.0D, Math.min(95.0D, config.getDouble("lottery.payout_percent", 90.0D)));
}
public record BuyResult(boolean success, String message, int round, int tickets, long cost) {
public static BuyResult fail(String message) {
return new BuyResult(false, message, 0, 0, 0L);
}
public static BuyResult ok(int round, int tickets, long cost) {
return new BuyResult(true, "", round, tickets, cost);
}
}
public record Status(int round, int players, int tickets, long pot) {
}
public record DrawResult(boolean success, String message, int round, String winnerName, long pot, int tickets) {
public static DrawResult fail(String message) {
return new DrawResult(false, message, 0, "", 0L, 0);
}
public static DrawResult ok(int round, String winnerName, long pot, int tickets) {
return new DrawResult(true, "", round, winnerName, pot, tickets);
}
}
}

View File

@@ -2,6 +2,7 @@ package com.librewiki.coalgov.service;
import com.librewiki.coalgov.CoalGovPlugin;
import com.librewiki.coalgov.storage.MarketRepository;
import com.librewiki.coalgov.storage.MarketRepository.BuybackLot;
import com.librewiki.coalgov.storage.MarketRepository.Flow;
import com.librewiki.coalgov.util.CoalMoney;
import org.bukkit.Bukkit;
@@ -58,7 +59,11 @@ public final class MarketService {
Map<Material, Double> prices = basePrices();
Map<Material, Long> simulatedStock = new LinkedHashMap<>();
Map<Material, Long> sold = new LinkedHashMap<>();
Map<Material, Long> buybackSold = new LinkedHashMap<>();
Map<Long, Long> buybackLotUse = new LinkedHashMap<>();
long grossCredit = 0L;
long buybackCredit = 0L;
long now = System.currentTimeMillis();
for (ItemStack item : player.getInventory().getStorageContents()) {
if (item == null || item.getType().isAir()) {
continue;
@@ -73,17 +78,40 @@ public final class MarketService {
: repository.stock(material, initialStock(material));
Flow flow = repository.flow(material, flowHalfLifeMillis(material));
long alreadySold = sold.getOrDefault(material, 0L);
grossCredit += sellCredit(material, basePrice, stock, flow, alreadySold, item.getAmount());
int regularAmount = item.getAmount();
long protectedAmount = 0L;
if (plugin.getConfig().getBoolean("market.buyback_protection.enabled", true)) {
for (BuybackLot lot : repository.buybackLots(player.getUniqueId(), material, now)) {
if (regularAmount <= 0) {
break;
}
long available = lot.amount() - buybackLotUse.getOrDefault(lot.id(), 0L);
if (available <= 0L) {
continue;
}
long used = Math.min(regularAmount, available);
buybackLotUse.merge(lot.id(), used, Long::sum);
protectedAmount += used;
buybackCredit += used * lot.unitPrice();
regularAmount -= (int) used;
}
}
if (regularAmount > 0) {
grossCredit += sellCredit(material, basePrice, stock + protectedAmount, flow, alreadySold, regularAmount);
}
simulatedStock.put(material, stock + item.getAmount());
sold.merge(material, (long) item.getAmount(), Long::sum);
if (protectedAmount > 0L) {
buybackSold.merge(material, protectedAmount, Long::sum);
}
}
if (grossCredit <= 0L) {
if (grossCredit <= 0L && buybackCredit <= 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);
if (!plugin.governmentService().spendMarketTransaction(grossCredit)) {
long credit = Math.max(0L, grossCredit - tax) + buybackCredit;
if (!plugin.governmentService().spendMarketTransaction(grossCredit + buybackCredit)) {
player.sendMessage(plugin.messages().text("&cThe Ministry treasury cannot fund that market purchase right now."));
return;
}
@@ -98,7 +126,12 @@ public final class MarketService {
repository.addStock(entry.getKey(), entry.getValue());
repository.recordSell(entry.getKey(), entry.getValue(), flowHalfLifeMillis(entry.getKey()));
}
if (tax > 0L) {
for (var entry : buybackSold.entrySet()) {
repository.consumeBuybacks(player.getUniqueId(), entry.getKey(), entry.getValue());
}
if (buybackCredit > 0L) {
player.sendMessage(plugin.messages().text("&aSold resources for &f" + CoalMoney.format(credit) + " &aincluding &f" + CoalMoney.format(buybackCredit) + " &asame-day buyback protection."));
} else 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."));
@@ -146,6 +179,7 @@ public final class MarketService {
}
plugin.governmentService().collectMarketRevenue(total);
repository.recordBuy(material, amount, flowHalfLifeMillis(material));
recordBuyback(player, material, amount, total);
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 {
@@ -225,6 +259,15 @@ public final class MarketService {
return Math.max(1L, CoalMoney.fromCoal(quote.buyUnitPrice() * amount));
}
private void recordBuyback(Player player, Material material, int amount, long total) throws SQLException {
if (!plugin.getConfig().getBoolean("market.buyback_protection.enabled", true) || amount <= 0) {
return;
}
long hours = Math.max(1L, plugin.getConfig().getLong("market.buyback_protection.duration_hours", 24L));
long unitPrice = Math.max(1L, (long) Math.ceil(total / (double) amount));
repository.recordBuyback(player.getUniqueId(), material, amount, unitPrice, System.currentTimeMillis() + hours * 3_600_000L);
}
private double coalEquivalentFloor(Material material) {
if (material == Material.COAL || material == Material.CHARCOAL) {
return 1.0D;
@@ -374,7 +417,7 @@ public final class MarketService {
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.BAKED_POTATO, 0.30D);
prices.put(Material.BEETROOT, 0.20D);
prices.put(Material.MELON_SLICE, 0.15D);
prices.put(Material.SWEET_BERRIES, 0.20D);

View File

@@ -94,6 +94,18 @@ public final class NpcService {
repository.addInventory(npcId, material, amount);
}
public boolean setRole(long npcId, String role) throws SQLException {
return repository.setRole(npcId, role);
}
public boolean setHome(long npcId, Location location) throws SQLException {
return repository.setHome(npcId, location.getWorld().getName(), location.getX(), location.getY(), location.getZ());
}
public boolean setWork(long npcId, Location location) throws SQLException {
return repository.setWork(npcId, location.getWorld().getName(), location.getX(), location.getY(), location.getZ());
}
public long balance(CoalNpc npc) throws SQLException {
return plugin.economyService().balance(npc.accountUuid());
}
@@ -163,8 +175,22 @@ public final class NpcService {
npc.spawn(new Location(world, coalNpc.x(), coalNpc.y(), coalNpc.z()));
}
}
if (resting(coalNpc)) {
returnHome(coalNpc, npc);
continue;
}
if (!maintainNeeds(coalNpc, npcs)) {
returnHome(coalNpc, npc);
continue;
}
Location work = workLocation(coalNpc);
if (work != null && npc.isSpawned()
&& npc.getEntity().getLocation().distanceSquared(work) > workArrivalDistanceSquared()) {
npc.getNavigator().setTarget(work);
continue;
}
if (npc.isSpawned() && !npc.getNavigator().isNavigating()) {
Location target = randomPoint(zone.get());
Location target = randomWorkPoint(coalNpc, zone.get());
if (target != null) {
npc.getNavigator().setTarget(target);
}
@@ -190,6 +216,13 @@ public final class NpcService {
private WorkerRecipe recipeFor(CoalNpc npc) {
ConfigurationSection roles = plugin.getConfig().getConfigurationSection("npc.worker.roles");
if (roles != null) {
String explicitRole = npc.role() == null ? "" : npc.role();
if (!explicitRole.isBlank()) {
WorkerRecipe recipe = recipe(explicitRole, roles.getConfigurationSection(explicitRole));
if (recipe != null) {
return recipe;
}
}
String npcName = npc.name().toLowerCase();
for (String role : roles.getKeys(false)) {
if (npcName.contains(role.toLowerCase())) {
@@ -208,6 +241,152 @@ public final class NpcService {
return material == null || amount <= 0 ? null : new WorkerRecipe("default", "PRODUCE", material, amount, Map.of(), List.of(), true);
}
private boolean resting(CoalNpc coalNpc) {
if (!plugin.getConfig().getBoolean("npc.schedule.enabled", true)) {
return false;
}
World world = Bukkit.getWorld(coalNpc.world());
if (world == null) {
return false;
}
long dayTime = world.getTime() % 24000L;
long start = Math.floorMod(plugin.getConfig().getLong("npc.schedule.rest_start_tick", 13000L), 24000L);
long end = Math.floorMod(plugin.getConfig().getLong("npc.schedule.rest_end_tick", 23000L), 24000L);
if (start == end) {
return false;
}
if (start < end) {
return dayTime >= start && dayTime < end;
}
return dayTime >= start || dayTime < end;
}
private boolean maintainNeeds(CoalNpc npc, List<CoalNpc> npcs) throws SQLException {
if (!plugin.getConfig().getBoolean("npc.needs.enabled", true)) {
return true;
}
long now = System.currentTimeMillis();
long intervalMillis = Math.max(1L, plugin.getConfig().getLong("npc.needs.hunger_interval_minutes", 20L)) * 60_000L;
long lastFoodAt = npc.lastFoodAt() <= 0L ? now : npc.lastFoodAt();
int hunger = Math.max(0, Math.min(20, npc.hunger()));
if (now - lastFoodAt >= intervalMillis) {
long intervals = Math.min(24L, (now - lastFoodAt) / intervalMillis);
hunger = Math.max(0, hunger - (int) intervals);
lastFoodAt += intervals * intervalMillis;
}
int eatBelow = Math.max(1, Math.min(20, plugin.getConfig().getInt("npc.needs.eat_below_hunger", 14)));
if (hunger <= eatBelow) {
Material food = consumeFood(npc, npcs);
if (food != null) {
hunger = Math.min(20, hunger + foodValue(food));
lastFoodAt = now;
}
}
repository.updateNeeds(npc.id(), hunger, lastFoodAt);
return hunger > Math.max(0, plugin.getConfig().getInt("npc.needs.work_min_hunger", 6));
}
private Material consumeFood(CoalNpc npc, List<CoalNpc> npcs) throws SQLException {
for (Material food : foodPreferences(npc)) {
if (repository.removeInventory(npc.id(), food, 1L)) {
return food;
}
requestInput(npc, food, 1L, npcs);
if (repository.removeInventory(npc.id(), food, 1L)) {
return food;
}
if (buyFoodFromMarket(npc, food)) {
return food;
}
}
return null;
}
private boolean buyFoodFromMarket(CoalNpc npc, Material food) throws SQLException {
if (!plugin.getConfig().getBoolean("npc.needs.buy_food_from_market", true)) {
return false;
}
double price = plugin.marketService().basePrices().getOrDefault(food, 1.0D);
double markup = Math.max(0.0D, plugin.getConfig().getDouble("npc.needs.market_food_markup", 1.25D));
long cost = Math.max(1L, CoalMoney.fromCoal(price * markup));
if (!plugin.economyService().charge(npc.accountUuid(), cost, "npc_food")) {
return false;
}
plugin.governmentService().collectMarketRevenue(cost);
return true;
}
private List<Material> foodPreferences(CoalNpc npc) {
List<String> configured = plugin.getConfig().getStringList("npc.needs.food_preferences." + roleName(npc));
if (configured.isEmpty()) {
configured = plugin.getConfig().getStringList("npc.needs.default_food_preferences");
}
List<Material> foods = new ArrayList<>();
for (String raw : configured) {
Material material = Material.matchMaterial(raw);
if (material != null) {
foods.add(material);
}
}
return foods.isEmpty() ? List.of(Material.BREAD, Material.BAKED_POTATO, Material.APPLE) : foods;
}
private int foodValue(Material food) {
return Math.max(1, plugin.getConfig().getInt("npc.needs.food_values." + food.name(),
plugin.getConfig().getInt("npc.needs.default_food_value", 5)));
}
private String roleName(CoalNpc npc) {
if (npc.role() != null && !npc.role().isBlank()) {
return npc.role();
}
return npc.name().toLowerCase().replace(' ', '_');
}
private void returnHome(CoalNpc coalNpc, NPC npc) {
if (!npc.isSpawned()) {
return;
}
Location home = homeLocation(coalNpc);
if (home == null) {
return;
}
if (npc.getEntity().getLocation().distanceSquared(home) > 4.0D) {
npc.getNavigator().setTarget(home);
} else if (npc.getNavigator().isNavigating()) {
npc.getNavigator().cancelNavigation();
}
}
private Location homeLocation(CoalNpc coalNpc) {
World world = Bukkit.getWorld(coalNpc.world());
if (world == null) {
return null;
}
return new Location(world, coalNpc.x(), coalNpc.y(), coalNpc.z());
}
private Location workLocation(CoalNpc npc) {
if (npc.workWorld() == null || npc.workX() == null || npc.workY() == null || npc.workZ() == null) {
return null;
}
World world = Bukkit.getWorld(npc.workWorld());
return world == null ? null : new Location(world, npc.workX(), npc.workY(), npc.workZ());
}
private Location randomWorkPoint(CoalNpc npc, NpcZone zone) {
Location work = workLocation(npc);
if (work != null && random.nextInt(4) != 0) {
return work.clone().add(random.nextDouble() * 4.0D - 2.0D, 0.0D, random.nextDouble() * 4.0D - 2.0D);
}
return randomPoint(zone);
}
private double workArrivalDistanceSquared() {
double distance = Math.max(1.0D, plugin.getConfig().getDouble("npc.schedule.work_arrival_distance", 4.0D));
return distance * distance;
}
private WorkerRecipe recipe(String name, ConfigurationSection section) {
if (section == null) {
return null;

View File

@@ -0,0 +1,90 @@
package com.librewiki.coalgov.service;
import com.librewiki.coalgov.CoalGovPlugin;
import org.bukkit.Bukkit;
import org.bukkit.GameRule;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.WorldCreator;
import org.bukkit.WorldType;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.entity.Player;
public final class PreserveWorldService {
private final CoalGovPlugin plugin;
private FileConfiguration config;
public PreserveWorldService(CoalGovPlugin plugin) {
this.plugin = plugin;
this.config = plugin.getConfig();
}
public void setConfig(FileConfiguration config) {
this.config = config;
}
public boolean enabled() {
return config.getBoolean("preserve.enabled", true);
}
public String worldName() {
return config.getString("preserve.world", "preserve");
}
public boolean isPreserveWorld(World world) {
return world != null && world.getName().equals(worldName());
}
public boolean isPreserveWorld(Location location) {
return location != null && isPreserveWorld(location.getWorld());
}
public World loadPreserveWorld() {
if (!enabled()) {
return null;
}
World existing = Bukkit.getWorld(worldName());
if (existing != null) {
applyRules(existing);
return existing;
}
WorldCreator creator = new WorldCreator(worldName());
creator.environment(World.Environment.NORMAL);
creator.type(WorldType.NORMAL);
creator.generateStructures(false);
World world = creator.createWorld();
if (world != null) {
applyRules(world);
plugin.getLogger().info("Loaded preserve world: " + world.getName());
} else {
plugin.getLogger().warning("Could not load preserve world: " + worldName());
}
return world;
}
public boolean canBypass(Player player) {
return player != null && plugin.hasAdminBypass(player);
}
public Location teleportLocation() {
World world = loadPreserveWorld();
if (world == null) {
return null;
}
double x = config.getDouble("preserve.spawn.x", 0.5D);
double z = config.getDouble("preserve.spawn.z", 0.5D);
double y = config.getDouble("preserve.spawn.y", Double.NaN);
if (Double.isNaN(y)) {
y = world.getHighestBlockYAt((int) Math.floor(x), (int) Math.floor(z)) + 1.0D;
}
float yaw = (float) config.getDouble("preserve.spawn.yaw", 0.0D);
float pitch = (float) config.getDouble("preserve.spawn.pitch", 0.0D);
return new Location(world, x, y, z, yaw, pitch);
}
private void applyRules(World world) {
world.setGameRule(GameRule.MOB_GRIEFING, false);
world.setGameRule(GameRule.DO_FIRE_TICK, false);
world.setPVP(false);
}
}

View File

@@ -33,6 +33,9 @@ public final class SuperFurnaceService {
public void registerRecipe() {
Bukkit.removeRecipe(recipeKey);
if (!plugin.getConfig().getBoolean("super_furnace.recipe_enabled", false)) {
return;
}
ShapedRecipe recipe = new ShapedRecipe(recipeKey, item());
recipe.shape("SSS", "SFS", "SSS");
recipe.setIngredient('S', Material.STONE);
@@ -54,6 +57,14 @@ public final class SuperFurnaceService {
return item;
}
public boolean syntheticDiamondsEnabled() {
return plugin.getConfig().getBoolean("synthetic_diamonds.enabled", true);
}
public int syntheticDiamondCoalInput() {
return Math.max(2, plugin.getConfig().getInt("synthetic_diamonds.coal_input", 40));
}
public boolean isItem(ItemStack item) {
if (item == null || item.getType() != Material.FURNACE || !item.hasItemMeta()) {
return false;

View File

@@ -0,0 +1,59 @@
package com.librewiki.coalgov.service;
import com.librewiki.coalgov.storage.WebTokenRepository;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.entity.Player;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.sql.SQLException;
import java.util.HexFormat;
public final class WebTokenService {
private static final char[] TOKEN_CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789".toCharArray();
private final WebTokenRepository repository;
private final SecureRandom random = new SecureRandom();
private FileConfiguration config;
public WebTokenService(WebTokenRepository repository, FileConfiguration config) {
this.repository = repository;
this.config = config;
}
public void setConfig(FileConfiguration config) {
this.config = config;
}
public TokenIssue issue(Player player) throws SQLException {
String token = token();
long minutes = Math.max(1L, config.getLong("web.tokens.expire_minutes", 10L));
long expiresAt = System.currentTimeMillis() + minutes * 60_000L;
repository.create(player.getUniqueId(), hash(token), expiresAt);
return new TokenIssue(token, minutes);
}
private String token() {
return group() + "-" + group() + "-" + group();
}
private String group() {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 4; i++) {
builder.append(TOKEN_CHARS[random.nextInt(TOKEN_CHARS.length)]);
}
return builder.toString();
}
private String hash(String token) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
return HexFormat.of().formatHex(digest.digest(token.toUpperCase().replace(" ", "").getBytes()));
} catch (NoSuchAlgorithmException exception) {
throw new IllegalStateException(exception);
}
}
public record TokenIssue(String token, long expiresMinutes) {
}
}

View File

@@ -165,6 +165,34 @@ public final class ClaimRepository {
}
}
public boolean transferGovernmentOwned(UUID governmentOwner, long id, UUID newOwner, long purchaseCost) throws SQLException {
boolean originalAutoCommit = database.connection().getAutoCommit();
database.connection().setAutoCommit(false);
try {
try (PreparedStatement statement = database.connection().prepareStatement("""
UPDATE claims SET owner_uuid = ?, purchase_cost = ?
WHERE id = ? AND owner_uuid = ?
""")) {
statement.setString(1, newOwner.toString());
statement.setLong(2, purchaseCost);
statement.setLong(3, id);
statement.setString(4, governmentOwner.toString());
if (statement.executeUpdate() != 1) {
database.connection().rollback();
return false;
}
}
clearPermissions(id);
database.connection().commit();
return true;
} catch (SQLException exception) {
database.connection().rollback();
throw exception;
} finally {
database.connection().setAutoCommit(originalAutoCommit);
}
}
public void grantPermission(long claimId, UUID playerUuid, ClaimPermission permission) throws SQLException {
try (PreparedStatement statement = database.connection().prepareStatement("""
INSERT INTO claim_permissions(claim_id, player_uuid, permission, granted_at)

View File

@@ -141,6 +141,44 @@ public final class Database {
updated_at INTEGER NOT NULL
)
""");
statement.execute("""
CREATE TABLE IF NOT EXISTS market_buybacks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
player_uuid TEXT NOT NULL,
material TEXT NOT NULL,
amount INTEGER NOT NULL,
unit_price INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
created_at INTEGER NOT NULL
)
""");
statement.execute("""
CREATE TABLE IF NOT EXISTS lottery_rounds (
round INTEGER PRIMARY KEY,
drawn_at INTEGER,
winner_uuid TEXT,
pot INTEGER NOT NULL DEFAULT 0
)
""");
statement.execute("""
CREATE TABLE IF NOT EXISTS lottery_tickets (
round INTEGER NOT NULL,
player_uuid TEXT NOT NULL,
player_name TEXT NOT NULL,
tickets INTEGER NOT NULL DEFAULT 0,
updated_at INTEGER NOT NULL,
PRIMARY KEY (round, player_uuid)
)
""");
statement.execute("""
CREATE TABLE IF NOT EXISTS web_tokens (
token_hash TEXT PRIMARY KEY,
player_uuid TEXT NOT NULL,
expires_at INTEGER NOT NULL,
used_at INTEGER,
created_at INTEGER NOT NULL
)
""");
statement.execute("""
CREATE TABLE IF NOT EXISTS treasury (
id TEXT PRIMARY KEY,
@@ -228,8 +266,19 @@ public final class Database {
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)");
statement.execute("CREATE INDEX IF NOT EXISTS idx_market_buybacks_player ON market_buybacks(player_uuid, material, expires_at)");
statement.execute("CREATE INDEX IF NOT EXISTS idx_lottery_tickets_round ON lottery_tickets(round)");
statement.execute("CREATE INDEX IF NOT EXISTS idx_web_tokens_player ON web_tokens(player_uuid, expires_at)");
}
ensureColumn("claims", "purchase_cost", "INTEGER NOT NULL DEFAULT -1");
ensureColumn("claims", "display_name", "TEXT");
ensureColumn("coalgov_npcs", "role", "TEXT NOT NULL DEFAULT ''");
ensureColumn("coalgov_npcs", "work_world", "TEXT");
ensureColumn("coalgov_npcs", "work_x", "REAL");
ensureColumn("coalgov_npcs", "work_y", "REAL");
ensureColumn("coalgov_npcs", "work_z", "REAL");
ensureColumn("coalgov_npcs", "hunger", "INTEGER NOT NULL DEFAULT 20");
ensureColumn("coalgov_npcs", "last_food_at", "INTEGER NOT NULL DEFAULT 0");
migrateCoalBalancesToCents();
}

View File

@@ -0,0 +1,99 @@
package com.librewiki.coalgov.storage;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public final class LotteryRepository {
private final Database database;
public LotteryRepository(Database database) {
this.database = database;
}
public int activeRound() throws SQLException {
try (PreparedStatement select = database.connection().prepareStatement(
"SELECT round FROM lottery_rounds WHERE drawn_at IS NULL ORDER BY round DESC LIMIT 1")) {
try (ResultSet result = select.executeQuery()) {
if (result.next()) {
return result.getInt("round");
}
}
}
int nextRound = lastRound() + 1;
try (PreparedStatement insert = database.connection().prepareStatement(
"INSERT INTO lottery_rounds(round, pot) VALUES (?, 0)")) {
insert.setInt(1, nextRound);
insert.executeUpdate();
}
return nextRound;
}
public void addTickets(int round, UUID uuid, String name, int tickets) throws SQLException {
try (PreparedStatement statement = database.connection().prepareStatement("""
INSERT INTO lottery_tickets(round, player_uuid, player_name, tickets, updated_at)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(round, player_uuid) DO UPDATE SET
player_name = excluded.player_name,
tickets = tickets + excluded.tickets,
updated_at = excluded.updated_at
""")) {
statement.setInt(1, round);
statement.setString(2, uuid.toString());
statement.setString(3, name);
statement.setInt(4, tickets);
statement.setLong(5, System.currentTimeMillis());
statement.executeUpdate();
}
}
public List<TicketHolder> tickets(int round) throws SQLException {
try (PreparedStatement statement = database.connection().prepareStatement("""
SELECT player_uuid, player_name, tickets FROM lottery_tickets
WHERE round = ? AND tickets > 0
ORDER BY player_name
""")) {
statement.setInt(1, round);
List<TicketHolder> holders = new ArrayList<>();
try (ResultSet result = statement.executeQuery()) {
while (result.next()) {
holders.add(new TicketHolder(
UUID.fromString(result.getString("player_uuid")),
result.getString("player_name"),
result.getInt("tickets")
));
}
}
return holders;
}
}
public void markDrawn(int round, UUID winnerUuid, long pot) throws SQLException {
try (PreparedStatement statement = database.connection().prepareStatement("""
UPDATE lottery_rounds
SET drawn_at = ?, winner_uuid = ?, pot = ?
WHERE round = ? AND drawn_at IS NULL
""")) {
statement.setLong(1, System.currentTimeMillis());
statement.setString(2, winnerUuid.toString());
statement.setLong(3, pot);
statement.setInt(4, round);
statement.executeUpdate();
}
}
private int lastRound() throws SQLException {
try (PreparedStatement statement = database.connection().prepareStatement(
"SELECT COALESCE(MAX(round), 0) AS last_round FROM lottery_rounds")) {
try (ResultSet result = statement.executeQuery()) {
return result.next() ? result.getInt("last_round") : 0;
}
}
}
public record TicketHolder(UUID uuid, String name, int tickets) {
}
}

View File

@@ -5,6 +5,9 @@ import org.bukkit.Material;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public final class MarketRepository {
private final Database database;
@@ -95,6 +98,73 @@ public final class MarketRepository {
recordFlow(material, Math.max(0L, amount), 0L, halfLifeMillis);
}
public void recordBuyback(UUID playerUuid, Material material, long amount, long unitPrice, long expiresAt) throws SQLException {
if (amount <= 0L || unitPrice <= 0L) {
return;
}
try (PreparedStatement statement = database.connection().prepareStatement("""
INSERT INTO market_buybacks(player_uuid, material, amount, unit_price, expires_at, created_at)
VALUES (?, ?, ?, ?, ?, ?)
""")) {
statement.setString(1, playerUuid.toString());
statement.setString(2, material.name());
statement.setLong(3, amount);
statement.setLong(4, unitPrice);
statement.setLong(5, expiresAt);
statement.setLong(6, System.currentTimeMillis());
statement.executeUpdate();
}
}
public List<BuybackLot> buybackLots(UUID playerUuid, Material material, long now) throws SQLException {
try (PreparedStatement delete = database.connection().prepareStatement(
"DELETE FROM market_buybacks WHERE expires_at <= ? OR amount <= 0")) {
delete.setLong(1, now);
delete.executeUpdate();
}
try (PreparedStatement statement = database.connection().prepareStatement("""
SELECT id, amount, unit_price FROM market_buybacks
WHERE player_uuid = ? AND material = ? AND expires_at > ?
ORDER BY created_at, id
""")) {
statement.setString(1, playerUuid.toString());
statement.setString(2, material.name());
statement.setLong(3, now);
List<BuybackLot> lots = new ArrayList<>();
try (ResultSet result = statement.executeQuery()) {
while (result.next()) {
lots.add(new BuybackLot(result.getLong("id"), result.getLong("amount"), result.getLong("unit_price")));
}
}
return lots;
}
}
public void consumeBuybacks(UUID playerUuid, Material material, long amount) throws SQLException {
long remaining = amount;
for (BuybackLot lot : buybackLots(playerUuid, material, System.currentTimeMillis())) {
if (remaining <= 0L) {
return;
}
long used = Math.min(remaining, lot.amount());
if (used >= lot.amount()) {
try (PreparedStatement delete = database.connection().prepareStatement(
"DELETE FROM market_buybacks WHERE id = ?")) {
delete.setLong(1, lot.id());
delete.executeUpdate();
}
} else {
try (PreparedStatement update = database.connection().prepareStatement(
"UPDATE market_buybacks SET amount = amount - ? WHERE id = ?")) {
update.setLong(1, used);
update.setLong(2, lot.id());
update.executeUpdate();
}
}
remaining -= used;
}
}
public void recordSell(Material material, long amount, long halfLifeMillis) throws SQLException {
recordFlow(material, 0L, Math.max(0L, amount), halfLifeMillis);
}
@@ -133,4 +203,7 @@ public final class MarketRepository {
public record Flow(double recentBuys, double recentSells) {
}
public record BuybackLot(long id, long amount, long unitPrice) {
}
}

View File

@@ -148,6 +148,55 @@ public final class NpcRepository {
}
}
public boolean setRole(long id, String role) throws SQLException {
try (PreparedStatement statement = database.connection().prepareStatement(
"UPDATE coalgov_npcs SET role = ? WHERE id = ?")) {
statement.setString(1, role == null ? "" : role);
statement.setLong(2, id);
return statement.executeUpdate() == 1;
}
}
public boolean setHome(long id, String world, double x, double y, double z) throws SQLException {
try (PreparedStatement statement = database.connection().prepareStatement("""
UPDATE coalgov_npcs SET world = ?, x = ?, y = ?, z = ?
WHERE id = ?
""")) {
statement.setString(1, world);
statement.setDouble(2, x);
statement.setDouble(3, y);
statement.setDouble(4, z);
statement.setLong(5, id);
return statement.executeUpdate() == 1;
}
}
public boolean setWork(long id, String world, double x, double y, double z) throws SQLException {
try (PreparedStatement statement = database.connection().prepareStatement("""
UPDATE coalgov_npcs SET work_world = ?, work_x = ?, work_y = ?, work_z = ?
WHERE id = ?
""")) {
statement.setString(1, world);
statement.setDouble(2, x);
statement.setDouble(3, y);
statement.setDouble(4, z);
statement.setLong(5, id);
return statement.executeUpdate() == 1;
}
}
public void updateNeeds(long id, int hunger, long lastFoodAt) throws SQLException {
try (PreparedStatement statement = database.connection().prepareStatement("""
UPDATE coalgov_npcs SET hunger = ?, last_food_at = ?
WHERE id = ?
""")) {
statement.setInt(1, Math.max(0, Math.min(20, hunger)));
statement.setLong(2, lastFoodAt);
statement.setLong(3, id);
statement.executeUpdate();
}
}
public void addInventory(long npcId, Material material, long amount) throws SQLException {
if (amount <= 0L) {
return;
@@ -250,10 +299,22 @@ public final class NpcRepository {
result.getString("world"),
result.getDouble("x"),
result.getDouble("y"),
result.getDouble("z")
result.getDouble("z"),
result.getString("role"),
result.getString("work_world"),
nullableDouble(result, "work_x"),
nullableDouble(result, "work_y"),
nullableDouble(result, "work_z"),
result.getInt("hunger"),
result.getLong("last_food_at")
);
}
private Double nullableDouble(ResultSet result, String column) throws SQLException {
double value = result.getDouble(column);
return result.wasNull() ? null : value;
}
private List<ClaimPoint> vertices(long zoneId) throws SQLException {
List<ClaimPoint> points = new ArrayList<>();
try (PreparedStatement statement = database.connection().prepareStatement(

View File

@@ -0,0 +1,32 @@
package com.librewiki.coalgov.storage;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.UUID;
public final class WebTokenRepository {
private final Database database;
public WebTokenRepository(Database database) {
this.database = database;
}
public void create(UUID playerUuid, String tokenHash, long expiresAt) throws SQLException {
try (PreparedStatement cleanup = database.connection().prepareStatement(
"DELETE FROM web_tokens WHERE player_uuid = ? OR expires_at <= ? OR used_at IS NOT NULL")) {
cleanup.setString(1, playerUuid.toString());
cleanup.setLong(2, System.currentTimeMillis());
cleanup.executeUpdate();
}
try (PreparedStatement insert = database.connection().prepareStatement("""
INSERT INTO web_tokens(token_hash, player_uuid, expires_at, used_at, created_at)
VALUES (?, ?, ?, NULL, ?)
""")) {
insert.setString(1, tokenHash);
insert.setString(2, playerUuid.toString());
insert.setLong(3, expiresAt);
insert.setLong(4, System.currentTimeMillis());
insert.executeUpdate();
}
}
}

View File

@@ -17,6 +17,43 @@ npc:
trader:
max_discount_percent: 15
max_surcharge_percent: 20
schedule:
enabled: true
rest_start_tick: 13000
rest_end_tick: 23000
work_arrival_distance: 4.0
needs:
enabled: true
hunger_interval_minutes: 20
eat_below_hunger: 14
work_min_hunger: 6
buy_food_from_market: true
market_food_markup: 1.25
default_food_value: 5
default_food_preferences:
- BREAD
- BAKED_POTATO
- APPLE
food_preferences:
farmer:
- BREAD
- BAKED_POTATO
- CARROT
baker:
- BREAD
- APPLE
trader:
- BREAD
- COOKED_BEEF
- APPLE
food_values:
BREAD: 5
BAKED_POTATO: 5
APPLE: 4
CARROT: 3
COOKED_BEEF: 8
COOKED_PORKCHOP: 8
COOKED_CHICKEN: 6
worker:
tick_interval_seconds: 20
allow_default_production: false
@@ -89,6 +126,9 @@ divining_rod:
market:
treasury:
transaction_budget_percent: 25.0
buyback_protection:
enabled: true
duration_hours: 24
dynamic:
initial_stock: 1024
target_stock: 1024
@@ -154,7 +194,7 @@ market:
APPLE: 0.75
CARROT: 0.25
POTATO: 0.20
BAKED_POTATO: 0.45
BAKED_POTATO: 0.30
BEETROOT: 0.20
MELON_SLICE: 0.15
SWEET_BERRIES: 0.20
@@ -194,3 +234,32 @@ spawn:
messages:
prefix: "&8[&6CoalGov&8]&r "
super_furnace:
recipe_enabled: false
synthetic_diamonds:
enabled: true
coal_input: 40
name_tags:
recover_named_entity_tags: true
lottery:
ticket_cost: 1.0
max_tickets_per_buy: 64
payout_percent: 90.0
web:
tokens:
expire_minutes: 10
preserve:
enabled: true
world: preserve
spawn:
x: 0.5
y: 96.0
z: 0.5
yaw: 0.0
pitch: 0.0

View File

@@ -9,8 +9,12 @@ depend: [Citizens]
commands:
coal:
description: CoalGov economy commands.
usage: /coal <balance|deposit|withdraw|pay|fines>
usage: /coal <balance|deposit|withdraw|pay|fines|networth|lottery>
permission: coalgov.coal
auction:
description: CoalGov chat auctions.
usage: /auction <start|bid|status|cancel>
permission: coalgov.auction
claim:
description: CoalGov claim commands.
usage: /claim <wand|buy|info|list|show|appraise|sell|transfer|abandon|trust|untrust|permissions>
@@ -40,12 +44,15 @@ commands:
permission: coalgov.police
cgnpc:
description: CoalGov NPC commands.
usage: /cgnpc <zone|create|remove|list|stock|funds|haggle>
usage: /cgnpc <zone|create|remove|list|stock|funds|role|home|work|needs|haggle>
permission: coalgov.admin
coalgov:
description: CoalGov admin commands.
usage: /coalgov admin <balance|grant|take|reload|spawn|bypass|rod|treasury|tax>
permission: coalgov.admin
description: CoalGov commands.
usage: /coalgov <webtoken|admin>
preserve:
description: Teleport to the CoalGov wildlife preserve.
usage: /preserve [return]
permission: coalgov.preserve
permissions:
coalgov.admin:
@@ -54,6 +61,9 @@ permissions:
coalgov.coal:
description: Allows coal economy commands.
default: true
coalgov.auction:
description: Allows CoalGov chat auctions.
default: true
coalgov.claim:
description: Allows claim commands.
default: true
@@ -72,3 +82,6 @@ permissions:
coalgov.police:
description: Allows issuing police fines.
default: op
coalgov.preserve:
description: Allows teleporting to the wildlife preserve.
default: true