Add newer CoalGov update
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -3,6 +3,8 @@
|
||||
/staging/
|
||||
/CoalGov/build/
|
||||
/CoalGov/.gradle/
|
||||
/property-web/__pycache__/
|
||||
/property-web/session.secret
|
||||
*.jar
|
||||
*.db
|
||||
*.db-*
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -76,6 +76,12 @@ Admins can grant a divining rod for testing:
|
||||
|
||||
/coalgov admin rod
|
||||
|
||||
Admins can grant centralized utility items and synthetic mobs:
|
||||
|
||||
/coalgov admin furnace
|
||||
/coalgov admin mob wandering_trader [count]
|
||||
/coalgov admin mob trader_llama [count]
|
||||
|
||||
===== Admin Bypass Mode =====
|
||||
|
||||
Admins normally play under the same build, marker, and mining restrictions as citizens.
|
||||
@@ -98,6 +104,45 @@ After editing market config, run:
|
||||
|
||||
Existing market stock is stored in SQLite and is not reset by reload.
|
||||
|
||||
===== NPC Household Setup =====
|
||||
|
||||
Create the NPC and then set its home, work site, and role:
|
||||
|
||||
/cgnpc create worker Farmer farm
|
||||
/cgnpc home <id>
|
||||
/cgnpc work <id>
|
||||
/cgnpc role <id> farmer
|
||||
/cgnpc needs <id>
|
||||
|
||||
Homes and work sites use the admin's current location. Workers return home at rest time or when hunger is too low. During work hours they move toward the work location and perform the role configured under ''npc.worker.roles''.
|
||||
|
||||
NPCs need food if ''npc.needs.enabled'' is true. Seed worker inventories or fund accounts as needed:
|
||||
|
||||
/cgnpc stock <id> BREAD 16
|
||||
/cgnpc funds <id> 25
|
||||
|
||||
If an NPC has no food in inventory, it can request food from same-zone NPCs or buy configured food from the market using its own account.
|
||||
|
||||
===== Super Furnaces and Synthetic Diamonds =====
|
||||
|
||||
On this server the CoalGov Super Furnace recipe is disabled so the Ministry can control synthetic diamond production. Admins grant furnaces with:
|
||||
|
||||
/coalgov admin furnace
|
||||
|
||||
When enabled in config, Super Furnaces can convert 40 COAL input into 1 DIAMOND.
|
||||
|
||||
===== Lottery =====
|
||||
|
||||
Players can buy tickets with:
|
||||
|
||||
/coal lottery buy [tickets]
|
||||
|
||||
Admins draw with:
|
||||
|
||||
/coal lottery draw
|
||||
|
||||
The house edge is enforced in code. The payout defaults to 90%, is capped at 95%, and always leaves at least 1 Coal Cent for the treasury if tickets were sold.
|
||||
|
||||
===== Reloading Config =====
|
||||
|
||||
After editing plugins/CoalGov/config.yml, run:
|
||||
|
||||
@@ -6,8 +6,12 @@
|
||||
* /coal deposit - deposit all coal items and coal blocks from inventory.
|
||||
* /coal withdraw <amount> - withdraw a whole-coal amount into coal items.
|
||||
* /coal pay <player> <amount> - pay another online player; accepts decimal coal or cc.
|
||||
* /coal networth - estimate your balance, appraised inventory, and claim purchase value.
|
||||
* /coal fines - list unpaid fines.
|
||||
* /coal fines pay <id|all> - pay one fine or all unpaid fines.
|
||||
* /coal lottery status - show the current lottery round.
|
||||
* /coal lottery buy [tickets] - buy one or more lottery tickets.
|
||||
* /coal lottery draw - admin-only lottery draw.
|
||||
|
||||
===== Market =====
|
||||
|
||||
@@ -39,7 +43,13 @@ Right-click the rod to test nearby ground for ore signals below and around you.
|
||||
|
||||
===== Super Furnace =====
|
||||
|
||||
Craft a CoalGov Super Furnace with a furnace in the center and stone in the eight surrounding slots. It smelts 5x as fast and burns fuel 4x faster.
|
||||
CoalGov Super Furnaces smelt 5x as fast and burn fuel 4x faster. Crafting is disabled on this server so the Ministry can keep synthetic diamonds centralized.
|
||||
|
||||
Admins can grant a Super Furnace with:
|
||||
|
||||
* /coalgov admin furnace
|
||||
|
||||
When synthetic diamonds are enabled, a Super Furnace can convert 40 COAL input into 1 DIAMOND.
|
||||
|
||||
===== Land =====
|
||||
|
||||
@@ -56,6 +66,8 @@ Craft a CoalGov Super Furnace with a furnace in the center and stone in the eigh
|
||||
* /coalgov admin reload - reload config.yml.
|
||||
* /coalgov admin bypass <on|off|status> - toggle admin gameplay bypass for yourself.
|
||||
* /coalgov admin rod - grant yourself a divining rod for testing.
|
||||
* /coalgov admin furnace - grant yourself a CoalGov Super Furnace.
|
||||
* /coalgov admin mob <wandering_trader|trader_llama> [count] - spawn controlled synthetic trader mobs.
|
||||
* /coalgov admin treasury balance - show treasury balance.
|
||||
* /coalgov admin treasury grant <player> <amount> - grant coal from the treasury.
|
||||
* /coalgov admin tax exempt <player> <on|off|status> - manage transaction tax exemption.
|
||||
@@ -70,7 +82,11 @@ Craft a CoalGov Super Furnace with a furnace in the center and stone in the eigh
|
||||
* /cgnpc create <trader|worker> <name> <zone> - create a Citizens-backed CoalGov NPC.
|
||||
* /cgnpc stock <id> <material> <amount> - add inventory to an NPC.
|
||||
* /cgnpc funds <id> <amount> - add coal or Coal Cents to an NPC account.
|
||||
* /cgnpc role <id> <role> - set an explicit configured worker role.
|
||||
* /cgnpc home <id> - set an NPC home to your current location.
|
||||
* /cgnpc work <id> - set an NPC work location to your current location.
|
||||
* /cgnpc needs <id> - inspect NPC hunger, role, home, and work locations.
|
||||
* /cgnpc haggle <id> <buy|sell> <material> <amount> <offer> - negotiate with a trader.
|
||||
* /cgnpc list - list NPC zones and CoalGov NPCs.
|
||||
|
||||
Worker roles are matched from the NPC name. By default, a worker named ''Farmer'' harvests and replants mature crops in its zone, a worker named ''Baker'' consumes wheat to produce bread, and workers move surplus output to traders in the same NPC zone.
|
||||
Worker roles can be set with ''/cgnpc role'' or matched from the NPC name. By default, a ''farmer'' harvests and replants mature crops, a ''baker'' consumes wheat to produce bread, and workers move surplus output to traders in the same NPC zone.
|
||||
|
||||
@@ -14,6 +14,12 @@ CoalGov uses Coal Cents for sub-coal transactions. Balances are stored as intege
|
||||
|
||||
Each player has one balance stored by UUID in SQLite. A player row is created automatically when they join the server.
|
||||
|
||||
Players can estimate their CoalGov wealth with:
|
||||
|
||||
/coal networth
|
||||
|
||||
The estimate includes current balance, configured market value for appraised inventory items, and claim purchase value. It is an estimate, not a guaranteed sale price.
|
||||
|
||||
===== Deposits =====
|
||||
|
||||
Players run:
|
||||
@@ -58,13 +64,29 @@ The market lets players sell configured resources for coal or Coal Cents and buy
|
||||
|
||||
Market purchases and sales can charge a configured government tax. Tax-exempt players do not pay transaction tax.
|
||||
|
||||
Same-day buyback protection records recent market purchases. If a player sells the same material back within the configured window, those protected units are paid back at the recorded purchase unit price instead of the current dynamic sell price.
|
||||
|
||||
===== Lottery =====
|
||||
|
||||
Players can buy lottery tickets with coal:
|
||||
|
||||
/coal lottery status
|
||||
/coal lottery buy
|
||||
/coal lottery buy <tickets>
|
||||
|
||||
Admins draw the current round with:
|
||||
|
||||
/coal lottery draw
|
||||
|
||||
The lottery has a built-in house edge. The configured payout defaults to 90%, is capped in code at 95%, and always leaves at least 1 Coal Cent for the treasury when tickets were sold.
|
||||
|
||||
===== Treasury =====
|
||||
|
||||
The treasury receives market transaction tax and paid fines. Admins can inspect the treasury and issue treasury-funded grants.
|
||||
|
||||
===== NPC Economic Agents =====
|
||||
|
||||
CoalGov NPCs have their own coal accounts and inventory. Trader NPCs buy and sell from their own stock and balance. Worker NPCs move inside an assigned polygon zone and periodically produce configured goods into their own inventory.
|
||||
CoalGov NPCs have their own coal accounts and inventory. Trader NPCs buy and sell from their own stock and balance. Worker NPCs move between home and work, consume food, buy food if funded, request supplies from same-zone NPCs, and periodically produce configured goods into their own inventory. Hungry workers stop working and return home until fed.
|
||||
|
||||
===== Transactions =====
|
||||
|
||||
|
||||
@@ -39,6 +39,23 @@ When a player sells a full inventory, the whole batch does not receive the first
|
||||
|
||||
Food items start with Ministry stock 0 by default, so players must sell food into the market before anyone can buy it back out.
|
||||
|
||||
===== Buyback Protection =====
|
||||
|
||||
CoalGov records protected buyback lots when a player buys resources from the Ministry. If that same player sells the same material within the configured protection window, protected units are paid at the recorded buy unit price instead of the current dynamic sell price.
|
||||
|
||||
This is meant to reduce accidental buy/sell losses from misclicks or short-term mistake purchases. After the protection window expires, normal dynamic sell pricing applies.
|
||||
|
||||
Default config:
|
||||
|
||||
market:
|
||||
buyback_protection:
|
||||
enabled: true
|
||||
duration_hours: 24
|
||||
|
||||
===== NPC Food Demand =====
|
||||
|
||||
NPC workers can buy configured foods from the market with their own NPC coal account when they cannot feed themselves from NPC inventory or same-zone supply chains. This creates a small simulated food demand for stocked market goods and gives farms/bakers a reason to keep NPC supply chains moving.
|
||||
|
||||
===== Default Dynamic Settings =====
|
||||
|
||||
market:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
====== NPC Supply Chains ======
|
||||
====== NPC Workers, Traders, and Households ======
|
||||
|
||||
CoalGov NPCs use Citizens. They are server-side entities, so players do not need client mods.
|
||||
|
||||
@@ -8,11 +8,13 @@ NPCs are managed with:
|
||||
|
||||
===== NPC Types =====
|
||||
|
||||
* Trader - stores goods, has a coal account, and can haggle with players.
|
||||
* Worker - patrols an assigned NPC zone and performs configured labor.
|
||||
* Trader - stores goods, has a coal account, haggles with players, and can receive worker output.
|
||||
* Worker - has a role, home, work location, hunger, inventory, coal account, and configured labor.
|
||||
|
||||
Workers and traders have their own persisted CoalGov inventories. Supply chains move items between those inventories before goods reach players.
|
||||
|
||||
NPCs are meant to behave like small economic agents. A worker travels between home and work, eats food, buys food if funded, requests supplies from same-zone NPCs, and stops working if hunger gets too low.
|
||||
|
||||
===== Zones =====
|
||||
|
||||
NPCs work inside polygon zones. Use the claim wand to mark points around the work area:
|
||||
@@ -25,6 +27,15 @@ Create NPCs while standing where they should spawn:
|
||||
/cgnpc create worker <name> <zone>
|
||||
/cgnpc create trader <name> <zone>
|
||||
|
||||
After creating an NPC, set its household and job details:
|
||||
|
||||
/cgnpc home <id>
|
||||
/cgnpc work <id>
|
||||
/cgnpc role <id> <role>
|
||||
/cgnpc needs <id>
|
||||
|
||||
''/cgnpc home'' and ''/cgnpc work'' use the admin's current location. Home is where an NPC returns during rest hours or when needs are too low. Work is the point the NPC prefers during working hours.
|
||||
|
||||
===== Farm-To-Market Example =====
|
||||
|
||||
Select a polygon around the farm with ''/claim wand'', then create an NPC zone:
|
||||
@@ -37,12 +48,23 @@ Create workers and a trader in that zone:
|
||||
/cgnpc create worker Baker farm
|
||||
/cgnpc create trader Marketman farm
|
||||
|
||||
Assign homes, work locations, and explicit roles:
|
||||
|
||||
/cgnpc home <farmerId>
|
||||
/cgnpc work <farmerId>
|
||||
/cgnpc role <farmerId> farmer
|
||||
/cgnpc home <bakerId>
|
||||
/cgnpc work <bakerId>
|
||||
/cgnpc role <bakerId> baker
|
||||
|
||||
The default supply chain is:
|
||||
|
||||
* Farmer scans the farm zone for mature WHEAT, CARROTS, POTATOES, and BEETROOTS.
|
||||
* Farmer walks to mature crops, harvests drops into NPC inventory, and replants if it has the seed or crop item.
|
||||
* Baker pulls WHEAT from same-zone NPC inventories.
|
||||
* Baker consumes 3 WHEAT to produce 2 BREAD.
|
||||
* Workers eat from their own inventory, then request food from same-zone NPCs, then buy configured food from the market if funded.
|
||||
* Workers that are too hungry return home and stop producing until fed.
|
||||
* Workers keep a reserve and move surplus output to same-zone traders.
|
||||
* Players can haggle with traders for stocked goods.
|
||||
|
||||
@@ -66,6 +88,32 @@ If a worker cannot replant, check its inventory and seed stock:
|
||||
|
||||
/cgnpc stock <id> WHEAT_SEEDS 64
|
||||
|
||||
===== Homes, Work, and Needs =====
|
||||
|
||||
Each CoalGov NPC stores:
|
||||
|
||||
* Home world and location.
|
||||
* Optional work world and location.
|
||||
* Explicit role name.
|
||||
* Hunger from 0 to 20.
|
||||
* Last food time.
|
||||
|
||||
Workers rest during configured night hours. During rest they return to home and do not produce goods. During work hours they prefer their work location and wander nearby, while still using their assigned NPC zone for crop scanning and supply-chain transfers.
|
||||
|
||||
Needs are configured under ''npc.needs''. Hunger decays on a timer. When hunger is low, the NPC attempts to eat configured foods:
|
||||
|
||||
* First from its own inventory.
|
||||
* Then by requesting food from same-zone NPC inventories.
|
||||
* Then by buying food from the market with its NPC coal account.
|
||||
|
||||
If the NPC cannot feed itself and hunger falls below the work threshold, it returns home and stops working.
|
||||
|
||||
Useful admin commands:
|
||||
|
||||
/cgnpc needs <id>
|
||||
/cgnpc stock <id> BREAD 16
|
||||
/cgnpc funds <id> 25
|
||||
|
||||
===== Traders =====
|
||||
|
||||
Traders sell from their own inventory. Workers in the same zone can stock traders automatically up to the configured restock target.
|
||||
@@ -81,10 +129,26 @@ Admins can add starting goods or coal:
|
||||
|
||||
===== Configuration =====
|
||||
|
||||
Worker roles are matched by NPC name under ''npc.worker.roles''. For example, an NPC named ''Farmer Joe'' uses the ''farmer'' role.
|
||||
Workers whose names do not match a configured role do nothing by default.
|
||||
Worker roles can be explicit or automatic. ''/cgnpc role <id> farmer'' sets a role directly. If no role is set, CoalGov tries to match the NPC name under ''npc.worker.roles''. For example, an NPC named ''Farmer Joe'' uses the ''farmer'' role.
|
||||
Workers whose role or name does not match a configured role do nothing by default.
|
||||
|
||||
npc:
|
||||
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_preferences:
|
||||
- BREAD
|
||||
- BAKED_POTATO
|
||||
- APPLE
|
||||
worker:
|
||||
tick_interval_seconds: 20
|
||||
allow_default_production: false
|
||||
|
||||
@@ -9,9 +9,14 @@ Use this checklist after install or upgrade.
|
||||
* /coal deposit removes coal from inventory and increases balance.
|
||||
* /coal withdraw decreases balance and gives coal items.
|
||||
* /coal pay transfers balance.
|
||||
* /coal networth reports balance, inventory estimate, and claims estimate.
|
||||
* /coal lottery buy 1 charges a ticket.
|
||||
* /coal lottery status shows the active round and pot.
|
||||
* /coal lottery draw pays a winner and leaves a treasury edge.
|
||||
* /market opens the Ministry Exchange inventory menu.
|
||||
* Selling resources through /market increases balance and market stock.
|
||||
* Buying resources through /market decreases balance and market stock.
|
||||
* Selling a recently bought material uses buyback protection during the configured window.
|
||||
* Market prices change after stock changes.
|
||||
* /land create creates a region.
|
||||
* /land info reports current land class.
|
||||
@@ -35,3 +40,10 @@ Use this checklist after install or upgrade.
|
||||
* /coalgov admin bypass on bypasses mining, build, and marker restrictions for that admin.
|
||||
* /coalgov admin bypass off restores normal gameplay restrictions for that admin.
|
||||
* /coalgov admin spawn setup creates protected spawn guide signs.
|
||||
* /coalgov admin furnace grants a Super Furnace.
|
||||
* Super Furnace synthetic diamond conversion works when enabled.
|
||||
* Named entities drop a recovered name tag on death.
|
||||
* /cgnpc role, /cgnpc home, /cgnpc work, and /cgnpc needs work for an NPC.
|
||||
* Worker NPCs return home during rest hours.
|
||||
* Worker NPCs travel toward work during work hours.
|
||||
* Worker NPCs consume configured food and stop working if hunger is too low.
|
||||
|
||||
@@ -10,13 +10,35 @@ services:
|
||||
VERSION: "1.21.11"
|
||||
TYPE: "PAPER"
|
||||
LEVEL: "world"
|
||||
MEMORY: "8G"
|
||||
MEMORY: "6G"
|
||||
USE_AIKAR_FLAGS: "true"
|
||||
ENABLE_RCON: "true"
|
||||
RCON_PASSWORD: "mcworldgen"
|
||||
RCON_PASSWORD: "poopymcbuttface69"
|
||||
VIEW_DISTANCE: "10"
|
||||
SIMULATION_DISTANCE: "6"
|
||||
PAUSE_WHEN_EMPTY_SECONDS: "-1"
|
||||
MOTD: "MC Worldgen test map"
|
||||
GENERATE_STRUCTURES: "false"
|
||||
volumes:
|
||||
- ./data:/data
|
||||
- /opt/bluemap/bluemap/web/maps/overworld/live:/bluemap-live
|
||||
|
||||
property-web:
|
||||
image: python:3.12-slim
|
||||
container_name: coalgov-property-web
|
||||
restart: unless-stopped
|
||||
working_dir: /app
|
||||
command: python app.py
|
||||
environment:
|
||||
COALGOV_DB: /data/plugins/CoalGov/coalgov.db
|
||||
COALGOV_WEB_SECRET: /app/session.secret
|
||||
COALGOV_WEB_PORT: "8088"
|
||||
COALGOV_BLUEMAP_URL: /bluemap/
|
||||
COALGOV_BLUEMAP_MARKERS_PATH: /bluemap-live/markers.json
|
||||
COALGOV_BLUEMAP_SYNC_SECONDS: "30"
|
||||
volumes:
|
||||
- ./property-web:/app
|
||||
- ./data:/data
|
||||
- /opt/bluemap/bluemap/web/maps/overworld/live:/bluemap-live
|
||||
ports:
|
||||
- "8088:8088"
|
||||
|
||||
24
property-web/README.md
Normal file
24
property-web/README.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# CoalGov Property Web
|
||||
|
||||
Small stdlib Python web app for the CoalGov SQLite database.
|
||||
|
||||
Features:
|
||||
|
||||
- `/coalgov webtoken` login support through the `web_tokens` table.
|
||||
- Browser map backed by the live BlueMap web UI.
|
||||
- BlueMap marker sync for claims and land regions.
|
||||
- Owned/manageable claims panel.
|
||||
- Rename claim display names.
|
||||
- Transfer claims to known CoalGov players by name or UUID.
|
||||
- Pay existing claim tax debt from the logged-in player's CoalGov balance.
|
||||
|
||||
Run on the server:
|
||||
|
||||
```bash
|
||||
cd /opt/mcworldgen-server/property-web
|
||||
COALGOV_DB=/opt/mcworldgen-server/data/plugins/CoalGov/coalgov.db \
|
||||
COALGOV_WEB_PORT=8088 \
|
||||
COALGOV_BLUEMAP_URL=http://example.org:8100/ \
|
||||
COALGOV_BLUEMAP_MARKERS_PATH=/opt/bluemap/bluemap/web/maps/overworld/live/markers.json \
|
||||
python3 app.py
|
||||
```
|
||||
491
property-web/app.py
Normal file
491
property-web/app.py
Normal file
@@ -0,0 +1,491 @@
|
||||
#!/usr/bin/env python3
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import html
|
||||
import json
|
||||
import os
|
||||
import secrets
|
||||
import sqlite3
|
||||
import threading
|
||||
import time
|
||||
from http import HTTPStatus
|
||||
from http.cookies import SimpleCookie
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent
|
||||
DB_PATH = Path(os.environ.get("COALGOV_DB", "/opt/mcworldgen-server/data/plugins/CoalGov/coalgov.db"))
|
||||
SECRET_PATH = Path(os.environ.get("COALGOV_WEB_SECRET", str(BASE_DIR / "session.secret")))
|
||||
HOST = os.environ.get("COALGOV_WEB_HOST", "0.0.0.0")
|
||||
PORT = int(os.environ.get("COALGOV_WEB_PORT", "8088"))
|
||||
SESSION_TTL = int(os.environ.get("COALGOV_WEB_SESSION_SECONDS", "43200"))
|
||||
BLUEMAP_URL = os.environ.get("COALGOV_BLUEMAP_URL", "")
|
||||
BLUEMAP_MARKERS_PATH = os.environ.get("COALGOV_BLUEMAP_MARKERS_PATH", "")
|
||||
BLUEMAP_SYNC_SECONDS = int(os.environ.get("COALGOV_BLUEMAP_SYNC_SECONDS", "30"))
|
||||
|
||||
|
||||
def secret():
|
||||
if SECRET_PATH.exists():
|
||||
return SECRET_PATH.read_bytes().strip()
|
||||
value = secrets.token_bytes(32)
|
||||
SECRET_PATH.write_bytes(base64.urlsafe_b64encode(value))
|
||||
os.chmod(SECRET_PATH, 0o600)
|
||||
return SECRET_PATH.read_bytes().strip()
|
||||
|
||||
|
||||
SECRET = secret()
|
||||
|
||||
|
||||
def db():
|
||||
con = sqlite3.connect(DB_PATH)
|
||||
con.row_factory = sqlite3.Row
|
||||
return con
|
||||
|
||||
|
||||
def now_ms():
|
||||
return int(time.time() * 1000)
|
||||
|
||||
|
||||
def token_hash(token):
|
||||
return hashlib.sha256(token.upper().replace(" ", "").encode()).hexdigest()
|
||||
|
||||
|
||||
def sign(payload):
|
||||
raw = json.dumps(payload, separators=(",", ":")).encode()
|
||||
body = base64.urlsafe_b64encode(raw).decode().rstrip("=")
|
||||
sig = hmac.new(SECRET, body.encode(), hashlib.sha256).hexdigest()
|
||||
return f"{body}.{sig}"
|
||||
|
||||
|
||||
def unsign(value):
|
||||
if not value or "." not in value:
|
||||
return None
|
||||
body, sig = value.rsplit(".", 1)
|
||||
expected = hmac.new(SECRET, body.encode(), hashlib.sha256).hexdigest()
|
||||
if not hmac.compare_digest(sig, expected):
|
||||
return None
|
||||
try:
|
||||
raw = base64.urlsafe_b64decode(body + "=" * (-len(body) % 4))
|
||||
payload = json.loads(raw)
|
||||
except (ValueError, json.JSONDecodeError):
|
||||
return None
|
||||
if payload.get("exp", 0) < int(time.time()):
|
||||
return None
|
||||
return payload
|
||||
|
||||
|
||||
def money(cents):
|
||||
cents = int(cents or 0)
|
||||
coal, cc = divmod(abs(cents), 100)
|
||||
prefix = "-" if cents < 0 else ""
|
||||
if coal and cc:
|
||||
return f"{prefix}{coal} coal {cc}cc"
|
||||
if coal:
|
||||
return f"{prefix}{coal} coal"
|
||||
return f"{prefix}{cc}cc"
|
||||
|
||||
|
||||
def rows(query, args=()):
|
||||
with db() as con:
|
||||
return [dict(row) for row in con.execute(query, args).fetchall()]
|
||||
|
||||
|
||||
def row(query, args=()):
|
||||
with db() as con:
|
||||
found = con.execute(query, args).fetchone()
|
||||
return dict(found) if found else None
|
||||
|
||||
|
||||
def display_claim(claim):
|
||||
name = claim.get("display_name") or f"Claim #{claim['id']}"
|
||||
return {**claim, "display": name, "tax_due_text": money(claim.get("tax_due", 0))}
|
||||
|
||||
|
||||
def can_manage(con, uuid, claim_id):
|
||||
claim = con.execute("SELECT owner_uuid FROM claims WHERE id = ?", (claim_id,)).fetchone()
|
||||
if not claim:
|
||||
return False
|
||||
if claim["owner_uuid"] == uuid:
|
||||
return True
|
||||
grant = con.execute("""
|
||||
SELECT 1 FROM claim_permissions
|
||||
WHERE claim_id = ? AND player_uuid = ? AND permission = 'MANAGE'
|
||||
LIMIT 1
|
||||
""", (claim_id, uuid)).fetchone()
|
||||
return bool(grant)
|
||||
|
||||
|
||||
def color(kind):
|
||||
colors = {
|
||||
"HOMESTEAD": {"r": 102, "g": 170, "b": 204},
|
||||
"INDUSTRIAL": {"r": 204, "g": 153, "b": 102},
|
||||
"GOVERNMENT": {"r": 221, "g": 102, "b": 102},
|
||||
"PUBLIC": {"r": 119, "g": 204, "b": 119},
|
||||
"PROTECTED_PRESERVE": {"r": 102, "g": 204, "b": 102},
|
||||
"MINING_CONCESSION": {"r": 204, "g": 204, "b": 102},
|
||||
"BORDER_ZONE": {"r": 204, "g": 102, "b": 204},
|
||||
}
|
||||
return colors.get(kind, {"r": 170, "g": 170, "b": 170})
|
||||
|
||||
|
||||
def rect_shape(area):
|
||||
x1, x2 = sorted((int(area["x1"]), int(area["x2"])))
|
||||
z1, z2 = sorted((int(area["z1"]), int(area["z2"])))
|
||||
return [
|
||||
{"x": x1, "z": z1},
|
||||
{"x": x2 + 1, "z": z1},
|
||||
{"x": x2 + 1, "z": z2 + 1},
|
||||
{"x": x1, "z": z2 + 1},
|
||||
]
|
||||
|
||||
|
||||
def center(shape):
|
||||
return {
|
||||
"x": sum(p["x"] for p in shape) / len(shape),
|
||||
"y": 72,
|
||||
"z": sum(p["z"] for p in shape) / len(shape),
|
||||
}
|
||||
|
||||
|
||||
def marker_detail(title, lines):
|
||||
body = "".join(f"<div>{html.escape(str(line))}</div>" for line in lines)
|
||||
return f"<strong>{html.escape(str(title))}</strong>{body}"
|
||||
|
||||
|
||||
def build_bluemap_markers():
|
||||
with db() as con:
|
||||
claims = [display_claim(dict(r)) for r in con.execute("""
|
||||
SELECT c.*, p.name AS owner_name FROM claims c
|
||||
LEFT JOIN players p ON p.uuid = c.owner_uuid
|
||||
WHERE c.world = 'world'
|
||||
ORDER BY c.id
|
||||
""")]
|
||||
vertices = {}
|
||||
for r in con.execute("SELECT claim_id, x, z FROM claim_vertices ORDER BY claim_id, vertex_order"):
|
||||
vertices.setdefault(r["claim_id"], []).append({"x": int(r["x"]), "z": int(r["z"])})
|
||||
lands = [dict(r) for r in con.execute("SELECT * FROM land_regions WHERE world = 'world' ORDER BY name")]
|
||||
|
||||
claim_markers = []
|
||||
for claim in claims:
|
||||
shape = vertices.get(claim["id"]) or rect_shape(claim)
|
||||
c = color(claim["claim_type"])
|
||||
claim_markers.append({
|
||||
"id": f"claim-{claim['id']}",
|
||||
"type": "shape",
|
||||
"map": "overworld",
|
||||
"position": center(shape),
|
||||
"label": claim["display"],
|
||||
"detail": marker_detail(claim["display"], [
|
||||
f"Type: {claim['claim_type']}",
|
||||
f"Owner: {claim.get('owner_name') or claim['owner_uuid']}",
|
||||
f"Tax due: {claim['tax_due_text']}",
|
||||
]),
|
||||
"shape": shape,
|
||||
"shapeY": 72,
|
||||
"depthTest": False,
|
||||
"lineWidth": 3,
|
||||
"lineColor": {**c, "a": 1.0},
|
||||
"fillColor": {**c, "a": 0.28},
|
||||
"minDistance": 10.0,
|
||||
"maxDistance": 10000000.0,
|
||||
})
|
||||
|
||||
land_markers = []
|
||||
for land in lands:
|
||||
shape = rect_shape(land)
|
||||
c = color(land["land_class"])
|
||||
land_markers.append({
|
||||
"id": f"land-{land['id']}",
|
||||
"type": "shape",
|
||||
"map": "overworld",
|
||||
"position": center(shape),
|
||||
"label": land["name"],
|
||||
"detail": marker_detail(land["name"], [f"Class: {land['land_class']}"]),
|
||||
"shape": shape,
|
||||
"shapeY": 73,
|
||||
"depthTest": False,
|
||||
"lineWidth": 2,
|
||||
"lineColor": {**c, "a": 1.0},
|
||||
"fillColor": {**c, "a": 0.16},
|
||||
"minDistance": 10.0,
|
||||
"maxDistance": 10000000.0,
|
||||
})
|
||||
|
||||
return {
|
||||
"coalgov-claims": {
|
||||
"label": "CoalGov Claims",
|
||||
"toggleable": True,
|
||||
"defaultHidden": False,
|
||||
"sorting": 10,
|
||||
"markers": {marker["id"]: {k: v for k, v in marker.items() if k != "id"} for marker in claim_markers},
|
||||
},
|
||||
"coalgov-land": {
|
||||
"label": "CoalGov Land",
|
||||
"toggleable": True,
|
||||
"defaultHidden": False,
|
||||
"sorting": 11,
|
||||
"markers": {marker["id"]: {k: v for k, v in marker.items() if k != "id"} for marker in land_markers},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def sync_bluemap_markers_once():
|
||||
if not BLUEMAP_MARKERS_PATH:
|
||||
return False
|
||||
target = Path(BLUEMAP_MARKERS_PATH)
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp = target.with_suffix(target.suffix + ".tmp")
|
||||
tmp.write_text(json.dumps(build_bluemap_markers(), separators=(",", ":")))
|
||||
tmp.replace(target)
|
||||
return True
|
||||
|
||||
|
||||
def sync_bluemap_markers_loop():
|
||||
while True:
|
||||
try:
|
||||
sync_bluemap_markers_once()
|
||||
except Exception as exc:
|
||||
print(f"BlueMap marker sync failed: {exc}", flush=True)
|
||||
time.sleep(max(5, BLUEMAP_SYNC_SECONDS))
|
||||
|
||||
|
||||
INDEX = """<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>CoalGov Property Map</title>
|
||||
<style>
|
||||
body { margin: 0; font-family: system-ui, sans-serif; background: #111; color: #eee; }
|
||||
header { display: flex; gap: 12px; align-items: center; padding: 10px 14px; background: #202020; border-bottom: 1px solid #333; }
|
||||
h1 { font-size: 18px; margin: 0; }
|
||||
main { display: grid; grid-template-columns: 1fr 360px; height: calc(100vh - 48px); }
|
||||
#mapwrap { position: relative; min-width: 0; background: #151515; }
|
||||
#bluemap { width: 100%; height: 100%; border: 0; display: block; }
|
||||
aside { overflow: auto; padding: 14px; background: #1b1b1b; border-left: 1px solid #333; }
|
||||
input, button, select { background: #272727; color: #eee; border: 1px solid #444; border-radius: 4px; padding: 8px; }
|
||||
button { cursor: pointer; }
|
||||
.row { display: flex; gap: 8px; margin: 8px 0; }
|
||||
.card { border: 1px solid #333; border-radius: 6px; padding: 10px; margin: 10px 0; background: #202020; }
|
||||
.muted { color: #aaa; font-size: 13px; }
|
||||
.hidden { display: none; }
|
||||
@media (max-width: 900px) { main { grid-template-columns: 1fr; grid-template-rows: 55vh auto; } aside { border-left: 0; border-top: 1px solid #333; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header><h1>CoalGov Property Map</h1><span id="who" class="muted"></span></header>
|
||||
<main>
|
||||
<section id="mapwrap">
|
||||
<iframe id="bluemap"></iframe>
|
||||
</section>
|
||||
<aside>
|
||||
<section id="login">
|
||||
<h2>Token Login</h2>
|
||||
<p class="muted">Run <code>/coalgov webtoken</code> in game, then enter the single-use token here.</p>
|
||||
<div class="row"><input id="token" placeholder="ABCD-1234-EFGH"><button onclick="login()">Log in</button></div>
|
||||
</section>
|
||||
<section id="panel" class="hidden">
|
||||
<div class="row"><button onclick="loadAll()">Refresh</button><button onclick="syncBlueMap()">Sync BlueMap</button><button onclick="logout()">Log out</button></div>
|
||||
<p class="muted">The main pane is BlueMap. CoalGov property and land overlays are published into BlueMap's marker layer.</p>
|
||||
<h2>Your Claims</h2><div id="claims"></div>
|
||||
<h2>Land Notices</h2><div id="notices"></div>
|
||||
</section>
|
||||
</aside>
|
||||
</main>
|
||||
<script>
|
||||
const configuredBlueMapUrl = "__BLUEMAP_URL__";
|
||||
bluemap.src = configuredBlueMapUrl || `${location.protocol}//${location.hostname}:8100/`;
|
||||
let data = {claims: [], lands: []};
|
||||
async function api(path, opts={}) {
|
||||
const r = await fetch(path, {headers: {"Content-Type":"application/json"}, ...opts});
|
||||
if (!r.ok) throw new Error((await r.json()).error || r.statusText);
|
||||
return r.json();
|
||||
}
|
||||
async function login() {
|
||||
try { await api("/api/login", {method:"POST", body:JSON.stringify({token:document.getElementById("token").value})}); await loadAll(); }
|
||||
catch(e) { alert(e.message); }
|
||||
}
|
||||
async function logout() { await api("/api/logout", {method:"POST"}); location.reload(); }
|
||||
async function syncBlueMap() { try { await api("/api/bluemap/sync", {method:"POST"}); const frame = document.getElementById("bluemap"); frame.src = frame.src; } catch(e) { alert(e.message); } }
|
||||
async function loadAll() {
|
||||
const me = await api("/api/me");
|
||||
document.getElementById("who").textContent = me.name + " (" + me.uuid + ")";
|
||||
document.getElementById("login").classList.add("hidden");
|
||||
document.getElementById("panel").classList.remove("hidden");
|
||||
data = await api("/api/map");
|
||||
renderClaims();
|
||||
}
|
||||
function renderClaims() {
|
||||
claims.innerHTML = "";
|
||||
for (const c of data.claims.filter(c=>c.manageable)) {
|
||||
const div = document.createElement("div"); div.className = "card";
|
||||
div.innerHTML = `<b>${c.display}</b><div class="muted">${c.claim_type} ${c.world} X ${c.x1}..${c.x2} Z ${c.z1}..${c.z2}<br>Owner: ${c.owner_name || c.owner_uuid}<br>Tax due: ${c.tax_due_text}</div>
|
||||
<div class="row"><input value="${c.display}" id="n${c.id}"><button onclick="renameClaim(${c.id})">Rename</button></div>
|
||||
<div class="row"><input placeholder="target player name or UUID" id="t${c.id}"><button onclick="transferClaim(${c.id})">Transfer</button></div>
|
||||
<button onclick="payTax(${c.id})">Pay Claim Tax</button>`;
|
||||
claims.appendChild(div);
|
||||
}
|
||||
notices.innerHTML = data.lands.map(l => `<div class="card"><b>${l.name}</b><div class="muted">${l.land_class} ${l.world} X ${l.x1}..${l.x2} Z ${l.z1}..${l.z2}</div></div>`).join("");
|
||||
}
|
||||
async function renameClaim(id) { try { await api(`/api/claims/${id}/rename`, {method:"POST", body:JSON.stringify({name:document.getElementById("n"+id).value})}); await loadAll(); } catch(e) { alert(e.message); } }
|
||||
async function transferClaim(id) { try { await api(`/api/claims/${id}/transfer`, {method:"POST", body:JSON.stringify({target:document.getElementById("t"+id).value})}); await loadAll(); } catch(e) { alert(e.message); } }
|
||||
async function payTax(id) { try { const r = await api(`/api/claims/${id}/pay-tax`, {method:"POST"}); alert(r.message); await loadAll(); } catch(e) { alert(e.message); } }
|
||||
loadAll().catch(()=>{});
|
||||
</script>
|
||||
</body></html>""".replace("__BLUEMAP_URL__", html.escape(BLUEMAP_URL, quote=True))
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
path = urlparse(self.path).path
|
||||
if path == "/":
|
||||
return self.html(INDEX)
|
||||
if path == "/api/me":
|
||||
user = self.require_user()
|
||||
if user:
|
||||
player = row("SELECT name FROM players WHERE uuid = ?", (user["uuid"],)) or {}
|
||||
self.json({"uuid": user["uuid"], "name": player.get("name", user["uuid"])})
|
||||
return
|
||||
if path == "/api/map":
|
||||
user = self.require_user()
|
||||
if not user:
|
||||
return
|
||||
uuid = user["uuid"]
|
||||
with db() as con:
|
||||
claims = [display_claim(dict(r)) for r in con.execute("""
|
||||
SELECT c.*, p.name AS owner_name FROM claims c
|
||||
LEFT JOIN players p ON p.uuid = c.owner_uuid
|
||||
ORDER BY c.id
|
||||
""")]
|
||||
vertices = {}
|
||||
for r in con.execute("SELECT claim_id, x, z FROM claim_vertices ORDER BY claim_id, vertex_order"):
|
||||
vertices.setdefault(r["claim_id"], []).append({"x": r["x"], "z": r["z"]})
|
||||
for c in claims:
|
||||
c["vertices"] = vertices.get(c["id"], [])
|
||||
c["manageable"] = c["owner_uuid"] == uuid or bool(con.execute("""
|
||||
SELECT 1 FROM claim_permissions WHERE claim_id = ? AND player_uuid = ? AND permission = 'MANAGE'
|
||||
""", (c["id"], uuid)).fetchone())
|
||||
lands = [dict(r) for r in con.execute("SELECT * FROM land_regions ORDER BY name")]
|
||||
self.json({"claims": claims, "lands": lands})
|
||||
return
|
||||
self.error(HTTPStatus.NOT_FOUND, "Not found")
|
||||
|
||||
def do_POST(self):
|
||||
path = urlparse(self.path).path
|
||||
if path == "/api/login":
|
||||
body = self.body()
|
||||
token = body.get("token", "")
|
||||
hashed = token_hash(token)
|
||||
with db() as con:
|
||||
found = con.execute("""
|
||||
SELECT token_hash, player_uuid FROM web_tokens
|
||||
WHERE token_hash = ? AND expires_at > ? AND used_at IS NULL
|
||||
""", (hashed, now_ms())).fetchone()
|
||||
if not found:
|
||||
return self.error(HTTPStatus.UNAUTHORIZED, "Invalid or expired token")
|
||||
con.execute("UPDATE web_tokens SET used_at = ? WHERE token_hash = ?", (now_ms(), hashed))
|
||||
con.commit()
|
||||
cookie = f"cg_session={sign({'uuid': found['player_uuid'], 'exp': int(time.time()) + SESSION_TTL})}; HttpOnly; SameSite=Lax; Path=/"
|
||||
self.json({"ok": True}, headers={"Set-Cookie": cookie})
|
||||
return
|
||||
if path == "/api/logout":
|
||||
self.json({"ok": True}, headers={"Set-Cookie": "cg_session=; Max-Age=0; Path=/"})
|
||||
return
|
||||
user = self.require_user()
|
||||
if not user:
|
||||
return
|
||||
if path == "/api/bluemap/sync":
|
||||
if sync_bluemap_markers_once():
|
||||
return self.json({"ok": True})
|
||||
return self.error(HTTPStatus.BAD_REQUEST, "BlueMap marker sync is not configured")
|
||||
if path.startswith("/api/claims/") and path.endswith("/rename"):
|
||||
claim_id = int(path.split("/")[3])
|
||||
name = str(self.body().get("name", "")).strip()[:48]
|
||||
if not name:
|
||||
return self.error(HTTPStatus.BAD_REQUEST, "Name required")
|
||||
with db() as con:
|
||||
if not can_manage(con, user["uuid"], claim_id):
|
||||
return self.error(HTTPStatus.FORBIDDEN, "You cannot manage that claim")
|
||||
con.execute("UPDATE claims SET display_name = ? WHERE id = ?", (name, claim_id))
|
||||
con.commit()
|
||||
return self.json({"ok": True})
|
||||
if path.startswith("/api/claims/") and path.endswith("/transfer"):
|
||||
claim_id = int(path.split("/")[3])
|
||||
target = str(self.body().get("target", "")).strip()
|
||||
with db() as con:
|
||||
if not can_manage(con, user["uuid"], claim_id):
|
||||
return self.error(HTTPStatus.FORBIDDEN, "You cannot manage that claim")
|
||||
player = con.execute("SELECT uuid, name FROM players WHERE uuid = ? OR lower(name) = lower(?)", (target, target)).fetchone()
|
||||
if not player:
|
||||
return self.error(HTTPStatus.BAD_REQUEST, "Target player not found in CoalGov")
|
||||
con.execute("UPDATE claims SET owner_uuid = ? WHERE id = ?", (player["uuid"], claim_id))
|
||||
con.execute("DELETE FROM claim_permissions WHERE claim_id = ?", (claim_id,))
|
||||
con.commit()
|
||||
return self.json({"ok": True})
|
||||
if path.startswith("/api/claims/") and path.endswith("/pay-tax"):
|
||||
claim_id = int(path.split("/")[3])
|
||||
with db() as con:
|
||||
if not can_manage(con, user["uuid"], claim_id):
|
||||
return self.error(HTTPStatus.FORBIDDEN, "You cannot manage that claim")
|
||||
claim = con.execute("SELECT tax_due FROM claims WHERE id = ?", (claim_id,)).fetchone()
|
||||
due = int(claim["tax_due"] or 0) if claim else 0
|
||||
if due <= 0:
|
||||
return self.json({"ok": True, "message": "No tax due."})
|
||||
updated = con.execute("UPDATE players SET coal_balance = coal_balance - ? WHERE uuid = ? AND coal_balance >= ?", (due, user["uuid"], due)).rowcount
|
||||
if updated != 1:
|
||||
return self.error(HTTPStatus.BAD_REQUEST, f"Balance too low. Need {money(due)}.")
|
||||
con.execute("UPDATE claims SET tax_due = 0 WHERE id = ?", (claim_id,))
|
||||
con.execute("INSERT INTO transactions(from_uuid,to_uuid,amount,reason,created_at) VALUES (?,?,?,?,?)", (user["uuid"], None, due, "claim_tax_web", now_ms()))
|
||||
con.execute("INSERT INTO treasury(id,balance) VALUES ('main', ?) ON CONFLICT(id) DO UPDATE SET balance = balance + excluded.balance", (due,))
|
||||
con.commit()
|
||||
return self.json({"ok": True, "message": f"Paid {money(due)}."})
|
||||
self.error(HTTPStatus.NOT_FOUND, "Not found")
|
||||
|
||||
def require_user(self):
|
||||
cookie = SimpleCookie(self.headers.get("Cookie", ""))
|
||||
payload = unsign(cookie.get("cg_session").value if cookie.get("cg_session") else "")
|
||||
if not payload:
|
||||
self.error(HTTPStatus.UNAUTHORIZED, "Login required")
|
||||
return None
|
||||
return payload
|
||||
|
||||
def body(self):
|
||||
length = int(self.headers.get("Content-Length", "0"))
|
||||
if length <= 0:
|
||||
return {}
|
||||
return json.loads(self.rfile.read(length) or b"{}")
|
||||
|
||||
def html(self, content):
|
||||
raw = content.encode()
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(raw)))
|
||||
self.end_headers()
|
||||
self.wfile.write(raw)
|
||||
|
||||
def json(self, value, headers=None):
|
||||
raw = json.dumps(value).encode()
|
||||
self.send_response(200)
|
||||
for key, val in (headers or {}).items():
|
||||
self.send_header(key, val)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(raw)))
|
||||
self.end_headers()
|
||||
self.wfile.write(raw)
|
||||
|
||||
def error(self, status, message):
|
||||
raw = json.dumps({"error": message}).encode()
|
||||
self.send_response(status.value)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(raw)))
|
||||
self.end_headers()
|
||||
self.wfile.write(raw)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"CoalGov property web listening on {HOST}:{PORT}, db={DB_PATH}")
|
||||
if BLUEMAP_MARKERS_PATH:
|
||||
threading.Thread(target=sync_bluemap_markers_loop, daemon=True).start()
|
||||
ThreadingHTTPServer((HOST, PORT), Handler).serve_forever()
|
||||
Reference in New Issue
Block a user