Add Coal Cents economy
This commit is contained in:
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/data/
|
||||||
|
/backups/
|
||||||
|
/staging/
|
||||||
|
/CoalGov/build/
|
||||||
|
/CoalGov/.gradle/
|
||||||
|
*.jar
|
||||||
|
*.db
|
||||||
|
*.db-*
|
||||||
|
docker-compose.yml.bak-*
|
||||||
|
docker-compose.yml.pre-*
|
||||||
197
CoalGov/README.md
Normal file
197
CoalGov/README.md
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
# CoalGov
|
||||||
|
|
||||||
|
CoalGov is a server-side Paper/Bukkit plugin for vanilla Minecraft clients. It adds a small coal-backed economy, land classes, claims, mining permits, protected government land, and coal ore mining restrictions.
|
||||||
|
|
||||||
|
The ledger uses Coal Cents for sub-coal transactions: `100cc` equals `1 coal`. Commands that accept money still treat plain numbers as coal, so `2.5`, `2.50`, and `250cc` all mean `2 coal 50cc`.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew build
|
||||||
|
```
|
||||||
|
|
||||||
|
The jar is written to `build/libs/CoalGov-*.jar`.
|
||||||
|
|
||||||
|
## VPS Install
|
||||||
|
|
||||||
|
1. Build with `./gradlew build`.
|
||||||
|
2. Copy `build/libs/CoalGov-*.jar` to `/path/to/server/plugins/`.
|
||||||
|
3. Restart the Paper server.
|
||||||
|
4. Edit `plugins/CoalGov/config.yml`.
|
||||||
|
5. Run `/coalgov admin reload`.
|
||||||
|
|
||||||
|
This is a Paper/Bukkit plugin only. It does not require Forge, Fabric, resource packs, client mods, or custom launchers.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
- `/coal balance`
|
||||||
|
- `/coal deposit`
|
||||||
|
- `/coal withdraw <amount>`; withdrawals must be whole coal because they create coal items
|
||||||
|
- `/coal pay <player> <amount>`; accepts coal decimals or `cc`
|
||||||
|
- `/coal fines [list|pay <id|all>]`
|
||||||
|
- `/market`
|
||||||
|
- `/claim wand`
|
||||||
|
- `/claim buy <homestead|industrial> <radius>`
|
||||||
|
- `/claim buy <homestead|industrial>` after selecting a polygon with `/claim wand`
|
||||||
|
- `/claim info`
|
||||||
|
- `/claim list`
|
||||||
|
- `/claim show <id>`
|
||||||
|
- `/claim appraise [id]`
|
||||||
|
- `/claim sell [id]`
|
||||||
|
- `/claim transfer <id> <player>`
|
||||||
|
- `/claim abandon <id>`
|
||||||
|
- `/permit buy mining <regionName>`
|
||||||
|
- `/permit list`
|
||||||
|
- `/diviningrod`
|
||||||
|
- `/land info`
|
||||||
|
- `/land create <name> <landClass> <radius>`
|
||||||
|
- `/land delete <name>`
|
||||||
|
- `/land list`
|
||||||
|
- `/coalgov admin balance <player>`
|
||||||
|
- `/coalgov admin grant <player> <amount>`
|
||||||
|
- `/coalgov admin take <player> <amount>`
|
||||||
|
- `/coalgov admin reload`
|
||||||
|
- `/coalgov admin bypass <on|off|status>`
|
||||||
|
- `/coalgov admin rod`
|
||||||
|
- `/coalgov admin treasury <balance|grant <player> <amount>>`
|
||||||
|
- `/coalgov admin tax exempt <player> <on|off|status>`
|
||||||
|
- `/police fine <player> <amount> <reason>`
|
||||||
|
- `/cgnpc zone create <name>`
|
||||||
|
- `/cgnpc create <trader|worker> <name> <zone>`
|
||||||
|
- `/cgnpc stock <id> <material> <amount>`
|
||||||
|
- `/cgnpc funds <id> <amount>`
|
||||||
|
- `/cgnpc haggle <id> <buy|sell> <material> <amount> <offer>`
|
||||||
|
|
||||||
|
## Super Furnace
|
||||||
|
|
||||||
|
Craft a CoalGov Super Furnace with a furnace in the center and stone in the eight surrounding slots. Placed super furnaces are persisted in SQLite, smelt 5x as fast, and burn fuel 4x faster.
|
||||||
|
|
||||||
|
## Permissions
|
||||||
|
|
||||||
|
- `coalgov.admin`: admin commands, default `op`. Gameplay bypass is available with `/coalgov admin bypass on`.
|
||||||
|
- `coalgov.coal`: coal economy commands, default `true`
|
||||||
|
- `coalgov.claim`: claim commands, default `true`
|
||||||
|
- `coalgov.permit`: permit commands, default `true`
|
||||||
|
- `coalgov.land.info`: land info commands, default `true`
|
||||||
|
- `coalgov.market`: market trading, default `true`
|
||||||
|
- `coalgov.diviningrod`: divining rod command, default `true`
|
||||||
|
- `coalgov.police`: police fine commands, default `op`
|
||||||
|
|
||||||
|
## Claim Wand
|
||||||
|
|
||||||
|
Run `/claim wand` to receive a CoalGov claim wand. Right-click blocks to add polygon points. Once at least three points are selected, `/claim buy homestead` or `/claim buy industrial` buys the polygon instead of a radius claim. Sneak right-click with the wand to clear the current polygon. The plugin shows temporary particles between points and client-side gold marker block previews while selecting, and permanent protected marker blocks are placed at claim vertices after purchase.
|
||||||
|
|
||||||
|
## Market
|
||||||
|
|
||||||
|
`/market` opens a vanilla inventory menu. Players can sell configured resources to the Ministry or buy from Ministry stock. A normal click buys 1 item, and shift-click buys 64. Prices are dynamic: each material has a base price, but the live price rises when market stock is low and falls when stock is high. Buying and selling use Coal Cents internally, so cheap blocks like cobblestone can transact below 1 coal without forcing bulk purchases. Selling resources increases stock; buying resources decreases stock. Bulk sales are priced against simulated rising stock so one large sale cannot receive the first scarce-stock quote for every item. Coal-equivalent materials keep a floor based on direct coal value, charcoal conversion, or fuel value; arbitrary/generated resources like cobblestone have no forced 1-coal floor. The stock is persisted in SQLite.
|
||||||
|
|
||||||
|
## Example Config
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
economy:
|
||||||
|
starting_balance: 0
|
||||||
|
deposit_coal: true
|
||||||
|
deposit_coal_blocks: true
|
||||||
|
|
||||||
|
claims:
|
||||||
|
homestead:
|
||||||
|
base_cost: 32
|
||||||
|
cost_per_block: 0.05
|
||||||
|
max_radius: 45
|
||||||
|
first_claim:
|
||||||
|
free: true
|
||||||
|
max_radius: 12
|
||||||
|
industrial:
|
||||||
|
base_cost: 128
|
||||||
|
cost_per_block: 0.15
|
||||||
|
max_radius: 96
|
||||||
|
|
||||||
|
permits:
|
||||||
|
mining:
|
||||||
|
cost: 64
|
||||||
|
duration_hours: 72
|
||||||
|
|
||||||
|
market:
|
||||||
|
dynamic:
|
||||||
|
initial_stock: 1024
|
||||||
|
target_stock: 1024
|
||||||
|
min_multiplier: 0.35
|
||||||
|
max_multiplier: 3.0
|
||||||
|
sell_multiplier: 0.85
|
||||||
|
buy_multiplier: 1.15
|
||||||
|
prices:
|
||||||
|
DIRT: 0.01
|
||||||
|
COBBLESTONE: 0.02
|
||||||
|
SAND: 0.05
|
||||||
|
GRAVEL: 0.04
|
||||||
|
OAK_LOG: 0.25
|
||||||
|
SPRUCE_LOG: 0.25
|
||||||
|
BIRCH_LOG: 0.25
|
||||||
|
TORCH: 0.10
|
||||||
|
COAL: 1.0
|
||||||
|
COAL_BLOCK: 9.0
|
||||||
|
RAW_COPPER: 0.5
|
||||||
|
COPPER_INGOT: 1.0
|
||||||
|
RAW_IRON: 1.0
|
||||||
|
IRON_INGOT: 2.0
|
||||||
|
RAW_GOLD: 2.0
|
||||||
|
GOLD_INGOT: 4.0
|
||||||
|
DIAMOND: 32.0
|
||||||
|
EMERALD: 12.0
|
||||||
|
|
||||||
|
land:
|
||||||
|
default_land_class: FREEHOLD
|
||||||
|
deny_mining_in_government: true
|
||||||
|
deny_mining_in_protected_preserve: true
|
||||||
|
deny_mining_in_border_zone: true
|
||||||
|
|
||||||
|
spawn:
|
||||||
|
protection_radius: 32
|
||||||
|
guide:
|
||||||
|
create_government_region: true
|
||||||
|
lines:
|
||||||
|
- "COALGOV BASIN"
|
||||||
|
- "Free first"
|
||||||
|
- "homestead:"
|
||||||
|
- "/claim buy"
|
||||||
|
- "Use /claim wand"
|
||||||
|
- "for polygons"
|
||||||
|
- "Coal seams:"
|
||||||
|
- "redwood ridges"
|
||||||
|
- "Permits:"
|
||||||
|
- "/permit buy"
|
||||||
|
- "mining <region>"
|
||||||
|
- "/market trades"
|
||||||
|
|
||||||
|
messages:
|
||||||
|
prefix: "&8[&6CoalGov&8]&r "
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manual Test Checklist
|
||||||
|
|
||||||
|
- Server starts without errors.
|
||||||
|
- Database file is created at `plugins/CoalGov/coalgov.db`.
|
||||||
|
- Player joining creates a `players` row.
|
||||||
|
- `/coal balance` works.
|
||||||
|
- `/coal deposit` removes coal from inventory and increases balance by `100cc` per coal item.
|
||||||
|
- `/coal withdraw` decreases balance and gives coal items for whole-coal amounts.
|
||||||
|
- `/coal pay` transfers whole coal, decimal coal, and `cc` amounts.
|
||||||
|
- `/market` opens a vanilla inventory trading menu.
|
||||||
|
- Selling resources through `/market` increases market stock.
|
||||||
|
- Buying resources through `/market` decreases market stock and changes future prices.
|
||||||
|
- `/land create` creates a region.
|
||||||
|
- `/land info` reports current land class.
|
||||||
|
- `/claim buy` creates a claim and charges coal.
|
||||||
|
- `/claim appraise` appraises the claim you are standing in.
|
||||||
|
- `/claim sell` sells the claim you are standing in back to the government at appraised value.
|
||||||
|
- `/claim show <id>` displays a temporary particle outline.
|
||||||
|
- `/claim wand` draws a temporary polygon with particles and marker block previews.
|
||||||
|
- `/claim buy homestead` after wand selection creates a polygon claim.
|
||||||
|
- Claim corners are marked with protected gold blocks.
|
||||||
|
- Unclaimed land is protected from normal block placement and breaking.
|
||||||
|
- Overlapping claims are rejected.
|
||||||
|
- Mining coal in `PROTECTED_PRESERVE` is denied.
|
||||||
|
- Mining coal in `MINING_CONCESSION` without permit is denied.
|
||||||
|
- Buying a mining permit allows mining coal in that concession.
|
||||||
|
- Admin permission alone does not bypass gameplay restrictions.
|
||||||
|
- `/coalgov admin bypass on` bypasses build, mining, and marker restrictions for that admin session.
|
||||||
32
CoalGov/build.gradle
Normal file
32
CoalGov/build.gradle
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
plugins {
|
||||||
|
id 'java'
|
||||||
|
id 'com.gradleup.shadow' version '8.3.6'
|
||||||
|
}
|
||||||
|
|
||||||
|
group = 'com.librewiki'
|
||||||
|
version = '1.0.0'
|
||||||
|
|
||||||
|
java {
|
||||||
|
toolchain {
|
||||||
|
languageVersion = JavaLanguageVersion.of(21)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnly 'io.papermc.paper:paper-api:1.21.11-R0.1-SNAPSHOT'
|
||||||
|
compileOnly 'net.citizensnpcs:citizens-main:2.0.42-SNAPSHOT'
|
||||||
|
implementation 'org.xerial:sqlite-jdbc:3.49.1.0'
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType(JavaCompile).configureEach {
|
||||||
|
options.encoding = 'UTF-8'
|
||||||
|
options.release = 21
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.shadowJar {
|
||||||
|
archiveClassifier.set('')
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.build {
|
||||||
|
dependsOn tasks.shadowJar
|
||||||
|
}
|
||||||
27
CoalGov/settings.gradle
Normal file
27
CoalGov/settings.gradle
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
gradlePluginPortal()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencyResolutionManagement {
|
||||||
|
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
maven {
|
||||||
|
name = "papermc"
|
||||||
|
url = uri("https://repo.papermc.io/repository/maven-public/")
|
||||||
|
}
|
||||||
|
maven {
|
||||||
|
name = "citizens"
|
||||||
|
url = uri("https://maven.citizensnpcs.co/repo")
|
||||||
|
}
|
||||||
|
maven {
|
||||||
|
name = "alessiodp"
|
||||||
|
url = uri("https://repo.alessiodp.com/releases/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.name = "CoalGov"
|
||||||
251
CoalGov/src/main/java/com/librewiki/coalgov/CoalGovPlugin.java
Normal file
251
CoalGov/src/main/java/com/librewiki/coalgov/CoalGovPlugin.java
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
package com.librewiki.coalgov;
|
||||||
|
|
||||||
|
import com.librewiki.coalgov.command.AdminCommand;
|
||||||
|
import com.librewiki.coalgov.command.ClaimCommand;
|
||||||
|
import com.librewiki.coalgov.command.CoalCommand;
|
||||||
|
import com.librewiki.coalgov.command.DiviningRodCommand;
|
||||||
|
import com.librewiki.coalgov.command.LandCommand;
|
||||||
|
import com.librewiki.coalgov.command.MarketCommand;
|
||||||
|
import com.librewiki.coalgov.command.NpcCommand;
|
||||||
|
import com.librewiki.coalgov.command.PermitCommand;
|
||||||
|
import com.librewiki.coalgov.command.PoliceCommand;
|
||||||
|
import com.librewiki.coalgov.listener.BlockBreakListener;
|
||||||
|
import com.librewiki.coalgov.listener.BlockPlaceListener;
|
||||||
|
import com.librewiki.coalgov.listener.ClaimWandListener;
|
||||||
|
import com.librewiki.coalgov.listener.DiviningRodListener;
|
||||||
|
import com.librewiki.coalgov.listener.MarketListener;
|
||||||
|
import com.librewiki.coalgov.listener.NpcListener;
|
||||||
|
import com.librewiki.coalgov.listener.PlayerJoinListener;
|
||||||
|
import com.librewiki.coalgov.listener.SuperFurnaceListener;
|
||||||
|
import com.librewiki.coalgov.service.BuildProtectionService;
|
||||||
|
import com.librewiki.coalgov.service.ClaimMarkerService;
|
||||||
|
import com.librewiki.coalgov.service.ClaimSelectionService;
|
||||||
|
import com.librewiki.coalgov.service.ClaimService;
|
||||||
|
import com.librewiki.coalgov.service.ClaimVisualService;
|
||||||
|
import com.librewiki.coalgov.service.DiviningRodService;
|
||||||
|
import com.librewiki.coalgov.service.EconomyService;
|
||||||
|
import com.librewiki.coalgov.service.GovernmentService;
|
||||||
|
import com.librewiki.coalgov.service.LandService;
|
||||||
|
import com.librewiki.coalgov.service.LandAppraisalService;
|
||||||
|
import com.librewiki.coalgov.service.MarketService;
|
||||||
|
import com.librewiki.coalgov.service.MiningService;
|
||||||
|
import com.librewiki.coalgov.service.NpcService;
|
||||||
|
import com.librewiki.coalgov.service.OpenRouterService;
|
||||||
|
import com.librewiki.coalgov.service.PermitService;
|
||||||
|
import com.librewiki.coalgov.service.SpawnGuideService;
|
||||||
|
import com.librewiki.coalgov.service.SuperFurnaceService;
|
||||||
|
import com.librewiki.coalgov.service.TaxService;
|
||||||
|
import com.librewiki.coalgov.storage.ClaimRepository;
|
||||||
|
import com.librewiki.coalgov.storage.Database;
|
||||||
|
import com.librewiki.coalgov.storage.EconomyRepository;
|
||||||
|
import com.librewiki.coalgov.storage.GovernmentRepository;
|
||||||
|
import com.librewiki.coalgov.storage.LandRepository;
|
||||||
|
import com.librewiki.coalgov.storage.MarketRepository;
|
||||||
|
import com.librewiki.coalgov.storage.NpcRepository;
|
||||||
|
import com.librewiki.coalgov.storage.PermitRepository;
|
||||||
|
import com.librewiki.coalgov.storage.SuperFurnaceRepository;
|
||||||
|
import com.librewiki.coalgov.util.CoalMoney;
|
||||||
|
import com.librewiki.coalgov.util.Messages;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public final class CoalGovPlugin extends JavaPlugin {
|
||||||
|
private Database database;
|
||||||
|
private EconomyService economyService;
|
||||||
|
private GovernmentService governmentService;
|
||||||
|
private ClaimService claimService;
|
||||||
|
private ClaimMarkerService claimMarkerService;
|
||||||
|
private ClaimSelectionService claimSelectionService;
|
||||||
|
private ClaimVisualService claimVisualService;
|
||||||
|
private BuildProtectionService buildProtectionService;
|
||||||
|
private LandAppraisalService landAppraisalService;
|
||||||
|
private MarketService marketService;
|
||||||
|
private NpcService npcService;
|
||||||
|
private OpenRouterService openRouterService;
|
||||||
|
private LandService landService;
|
||||||
|
private PermitService permitService;
|
||||||
|
private MiningService miningService;
|
||||||
|
private DiviningRodService diviningRodService;
|
||||||
|
private SuperFurnaceService superFurnaceService;
|
||||||
|
private SpawnGuideService spawnGuideService;
|
||||||
|
private TaxService taxService;
|
||||||
|
private Messages messages;
|
||||||
|
private final Set<UUID> adminBypassPlayers = new HashSet<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable() {
|
||||||
|
saveDefaultConfig();
|
||||||
|
messages = new Messages(getConfig());
|
||||||
|
database = new Database(getDataFolder());
|
||||||
|
try {
|
||||||
|
database.open();
|
||||||
|
} catch (SQLException exception) {
|
||||||
|
getLogger().severe("Failed to initialize SQLite database: " + exception.getMessage());
|
||||||
|
getServer().getPluginManager().disablePlugin(this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
EconomyRepository economyRepository = new EconomyRepository(database);
|
||||||
|
LandRepository landRepository = new LandRepository(database);
|
||||||
|
ClaimRepository claimRepository = new ClaimRepository(database);
|
||||||
|
PermitRepository permitRepository = new PermitRepository(database);
|
||||||
|
MarketRepository marketRepository = new MarketRepository(database);
|
||||||
|
GovernmentRepository governmentRepository = new GovernmentRepository(database);
|
||||||
|
SuperFurnaceRepository superFurnaceRepository = new SuperFurnaceRepository(database);
|
||||||
|
NpcRepository npcRepository = new NpcRepository(database);
|
||||||
|
|
||||||
|
economyService = new EconomyService(economyRepository, CoalMoney.fromCoalConfig(getConfig().getDouble("economy.starting_balance", 0.0D)));
|
||||||
|
governmentService = new GovernmentService(governmentRepository, economyService, getConfig());
|
||||||
|
landService = new LandService(landRepository, getConfig());
|
||||||
|
claimService = new ClaimService(claimRepository, landService, economyService, getConfig());
|
||||||
|
claimMarkerService = new ClaimMarkerService(this, claimService);
|
||||||
|
claimSelectionService = new ClaimSelectionService(this);
|
||||||
|
claimVisualService = new ClaimVisualService(this);
|
||||||
|
buildProtectionService = new BuildProtectionService(this, claimService);
|
||||||
|
landAppraisalService = new LandAppraisalService(this, claimService);
|
||||||
|
marketService = new MarketService(this, marketRepository);
|
||||||
|
npcService = new NpcService(this, npcRepository);
|
||||||
|
openRouterService = new OpenRouterService(this);
|
||||||
|
permitService = new PermitService(permitRepository, landService, economyService, getConfig());
|
||||||
|
miningService = new MiningService(this, landService, claimService, permitService);
|
||||||
|
diviningRodService = new DiviningRodService(this);
|
||||||
|
superFurnaceService = new SuperFurnaceService(this, superFurnaceRepository);
|
||||||
|
spawnGuideService = new SpawnGuideService(this);
|
||||||
|
taxService = new TaxService();
|
||||||
|
|
||||||
|
Objects.requireNonNull(getCommand("coal")).setExecutor(new CoalCommand(this));
|
||||||
|
Objects.requireNonNull(getCommand("claim")).setExecutor(new ClaimCommand(this));
|
||||||
|
Objects.requireNonNull(getCommand("permit")).setExecutor(new PermitCommand(this));
|
||||||
|
Objects.requireNonNull(getCommand("land")).setExecutor(new LandCommand(this));
|
||||||
|
Objects.requireNonNull(getCommand("market")).setExecutor(new MarketCommand(this));
|
||||||
|
Objects.requireNonNull(getCommand("diviningrod")).setExecutor(new DiviningRodCommand(this));
|
||||||
|
Objects.requireNonNull(getCommand("police")).setExecutor(new PoliceCommand(this));
|
||||||
|
Objects.requireNonNull(getCommand("cgnpc")).setExecutor(new NpcCommand(this));
|
||||||
|
Objects.requireNonNull(getCommand("coalgov")).setExecutor(new AdminCommand(this));
|
||||||
|
|
||||||
|
getServer().getPluginManager().registerEvents(new BlockBreakListener(this), this);
|
||||||
|
getServer().getPluginManager().registerEvents(new BlockPlaceListener(this), this);
|
||||||
|
getServer().getPluginManager().registerEvents(new ClaimWandListener(this), this);
|
||||||
|
getServer().getPluginManager().registerEvents(new DiviningRodListener(this), this);
|
||||||
|
getServer().getPluginManager().registerEvents(new MarketListener(this), this);
|
||||||
|
getServer().getPluginManager().registerEvents(new NpcListener(this), this);
|
||||||
|
getServer().getPluginManager().registerEvents(new PlayerJoinListener(this), this);
|
||||||
|
getServer().getPluginManager().registerEvents(new SuperFurnaceListener(this), this);
|
||||||
|
superFurnaceService.registerRecipe();
|
||||||
|
getServer().getScheduler().runTaskLater(this, () -> claimMarkerService.placeAllLoadedMarkers(), 40L);
|
||||||
|
long npcTicks = Math.max(20L, getConfig().getLong("npc.worker.tick_interval_seconds", 20L) * 20L);
|
||||||
|
getServer().getScheduler().runTaskTimer(this, () -> npcService.tickWorkers(), 80L, npcTicks);
|
||||||
|
getLogger().info("CoalGov enabled.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisable() {
|
||||||
|
if (database != null) {
|
||||||
|
database.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reloadCoalGovConfig() {
|
||||||
|
reloadConfig();
|
||||||
|
messages = new Messages(getConfig());
|
||||||
|
governmentService.setConfig(getConfig());
|
||||||
|
landService.setConfig(getConfig());
|
||||||
|
claimService.setConfig(getConfig());
|
||||||
|
permitService.setConfig(getConfig());
|
||||||
|
superFurnaceService.registerRecipe();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasAdminBypass(Player player) {
|
||||||
|
return player.hasPermission("coalgov.admin") && adminBypassPlayers.contains(player.getUniqueId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAdminBypass(Player player, boolean enabled) {
|
||||||
|
if (enabled) {
|
||||||
|
adminBypassPlayers.add(player.getUniqueId());
|
||||||
|
} else {
|
||||||
|
adminBypassPlayers.remove(player.getUniqueId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public EconomyService economyService() {
|
||||||
|
return economyService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GovernmentService governmentService() {
|
||||||
|
return governmentService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClaimService claimService() {
|
||||||
|
return claimService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClaimVisualService claimVisualService() {
|
||||||
|
return claimVisualService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClaimMarkerService claimMarkerService() {
|
||||||
|
return claimMarkerService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClaimSelectionService claimSelectionService() {
|
||||||
|
return claimSelectionService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BuildProtectionService buildProtectionService() {
|
||||||
|
return buildProtectionService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LandAppraisalService landAppraisalService() {
|
||||||
|
return landAppraisalService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MarketService marketService() {
|
||||||
|
return marketService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public NpcService npcService() {
|
||||||
|
return npcService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OpenRouterService openRouterService() {
|
||||||
|
return openRouterService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LandService landService() {
|
||||||
|
return landService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PermitService permitService() {
|
||||||
|
return permitService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MiningService miningService() {
|
||||||
|
return miningService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DiviningRodService diviningRodService() {
|
||||||
|
return diviningRodService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SuperFurnaceService superFurnaceService() {
|
||||||
|
return superFurnaceService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SpawnGuideService spawnGuideService() {
|
||||||
|
return spawnGuideService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TaxService taxService() {
|
||||||
|
return taxService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Messages messages() {
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
package com.librewiki.coalgov.command;
|
||||||
|
|
||||||
|
import com.librewiki.coalgov.CoalGovPlugin;
|
||||||
|
import com.librewiki.coalgov.util.CoalMoney;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.OfflinePlayer;
|
||||||
|
import org.bukkit.command.Command;
|
||||||
|
import org.bukkit.command.CommandExecutor;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
public final class AdminCommand implements CommandExecutor {
|
||||||
|
private final CoalGovPlugin plugin;
|
||||||
|
|
||||||
|
public AdminCommand(CoalGovPlugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||||
|
if (args.length == 0 || !args[0].equalsIgnoreCase("admin")) {
|
||||||
|
usage(sender);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!sender.hasPermission("coalgov.admin")) {
|
||||||
|
sender.sendMessage(plugin.messages().text("&cYou lack permission."));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (args.length >= 2 && args[1].equalsIgnoreCase("reload")) {
|
||||||
|
plugin.reloadCoalGovConfig();
|
||||||
|
sender.sendMessage(plugin.messages().text("&aConfig reloaded."));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (args.length >= 2 && args[1].equalsIgnoreCase("bypass")) {
|
||||||
|
bypass(sender, args);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (args.length >= 2 && args[1].equalsIgnoreCase("spawn")) {
|
||||||
|
spawn(sender, args);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (args.length >= 2 && args[1].equalsIgnoreCase("rod")) {
|
||||||
|
rod(sender, args);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (args.length >= 2 && args[1].equalsIgnoreCase("treasury")) {
|
||||||
|
treasury(sender, args);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (args.length >= 2 && args[1].equalsIgnoreCase("tax")) {
|
||||||
|
tax(sender, args);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (args.length < 3) {
|
||||||
|
usage(sender);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
OfflinePlayer target = Bukkit.getOfflinePlayer(args[2]);
|
||||||
|
plugin.economyService().ensurePlayer(target);
|
||||||
|
switch (args[1].toLowerCase()) {
|
||||||
|
case "balance" -> sender.sendMessage(plugin.messages().text("&e" + args[2] + ": &f"
|
||||||
|
+ CoalMoney.format(plugin.economyService().balance(target.getUniqueId()))));
|
||||||
|
case "grant" -> grant(sender, target, args);
|
||||||
|
case "take" -> take(sender, target, args);
|
||||||
|
default -> usage(sender);
|
||||||
|
}
|
||||||
|
} catch (SQLException exception) {
|
||||||
|
plugin.getLogger().warning("Admin command failed: " + exception.getMessage());
|
||||||
|
sender.sendMessage(plugin.messages().text("&cCoalGov database unavailable."));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void usage(CommandSender sender) {
|
||||||
|
sender.sendMessage(plugin.messages().text("&eUsage: /coalgov admin <balance|grant|take|reload|spawn|bypass|rod|treasury|tax>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void bypass(CommandSender sender, String[] args) {
|
||||||
|
if (!(sender instanceof Player player)) {
|
||||||
|
sender.sendMessage("Players only.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (args.length == 2 || (args.length == 3 && args[2].equalsIgnoreCase("status"))) {
|
||||||
|
String status = plugin.hasAdminBypass(player) ? "&aON" : "&cOFF";
|
||||||
|
player.sendMessage(plugin.messages().text("&eAdmin bypass is " + status + "&e."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (args.length != 3) {
|
||||||
|
player.sendMessage(plugin.messages().text("&eUsage: /coalgov admin bypass <on|off|status>"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (args[2].equalsIgnoreCase("on")) {
|
||||||
|
plugin.setAdminBypass(player, true);
|
||||||
|
player.sendMessage(plugin.messages().text("&aAdmin bypass enabled. Build, mining, and marker restrictions are bypassed."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (args[2].equalsIgnoreCase("off")) {
|
||||||
|
plugin.setAdminBypass(player, false);
|
||||||
|
player.sendMessage(plugin.messages().text("&aAdmin bypass disabled. You now play under normal CoalGov rules."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
player.sendMessage(plugin.messages().text("&eUsage: /coalgov admin bypass <on|off|status>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void spawn(CommandSender sender, String[] args) throws SQLException {
|
||||||
|
if (!(sender instanceof Player player)) {
|
||||||
|
sender.sendMessage("Players only.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (args.length != 3 || !args[2].equalsIgnoreCase("setup")) {
|
||||||
|
player.sendMessage(plugin.messages().text("&eUsage: /coalgov admin spawn setup"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
plugin.spawnGuideService().setup(player);
|
||||||
|
player.sendMessage(plugin.messages().text("&aSpawn set, protected, and guide signs placed."));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void rod(CommandSender sender, String[] args) {
|
||||||
|
if (!(sender instanceof Player player)) {
|
||||||
|
sender.sendMessage("Players only.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (args.length != 2) {
|
||||||
|
player.sendMessage(plugin.messages().text("&eUsage: /coalgov admin rod"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
player.getInventory().addItem(plugin.diviningRodService().rod());
|
||||||
|
player.sendMessage(plugin.messages().text("&aDivining rod granted."));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void treasury(CommandSender sender, String[] args) throws SQLException {
|
||||||
|
if (args.length == 3 && args[2].equalsIgnoreCase("balance")) {
|
||||||
|
sender.sendMessage(plugin.messages().text("&eTreasury: &f" + CoalMoney.format(plugin.governmentService().treasuryBalance())));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (args.length == 5 && args[2].equalsIgnoreCase("grant")) {
|
||||||
|
OfflinePlayer target = Bukkit.getOfflinePlayer(args[3]);
|
||||||
|
plugin.economyService().ensurePlayer(target);
|
||||||
|
long amount = CoalMoney.parsePositive(args[4]);
|
||||||
|
if (amount <= 0L) {
|
||||||
|
sender.sendMessage(plugin.messages().text("&cAmount must be greater than zero."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!plugin.governmentService().grantFromTreasury(target.getUniqueId(), amount)) {
|
||||||
|
sender.sendMessage(plugin.messages().text("&cTreasury grant failed. Check treasury balance."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sender.sendMessage(plugin.messages().text("&aGranted &f" + CoalMoney.format(amount) + " &afrom the treasury."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sender.sendMessage(plugin.messages().text("&eUsage: /coalgov admin treasury <balance|grant <player> <amount>>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void tax(CommandSender sender, String[] args) throws SQLException {
|
||||||
|
if (args.length != 5 || !args[2].equalsIgnoreCase("exempt")) {
|
||||||
|
sender.sendMessage(plugin.messages().text("&eUsage: /coalgov admin tax exempt <player> <on|off|status>"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
OfflinePlayer target = Bukkit.getOfflinePlayer(args[3]);
|
||||||
|
plugin.economyService().ensurePlayer(target);
|
||||||
|
if (args[4].equalsIgnoreCase("status")) {
|
||||||
|
boolean exempt = plugin.governmentService().taxExempt(target.getUniqueId());
|
||||||
|
sender.sendMessage(plugin.messages().text("&eTax exemption for &f" + args[3] + "&e: " + (exempt ? "&aON" : "&cOFF")));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (args[4].equalsIgnoreCase("on")) {
|
||||||
|
plugin.governmentService().setTaxExempt(target.getUniqueId(), true);
|
||||||
|
sender.sendMessage(plugin.messages().text("&aTax exemption enabled for &f" + args[3] + "&a."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (args[4].equalsIgnoreCase("off")) {
|
||||||
|
plugin.governmentService().setTaxExempt(target.getUniqueId(), false);
|
||||||
|
sender.sendMessage(plugin.messages().text("&aTax exemption disabled for &f" + args[3] + "&a."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sender.sendMessage(plugin.messages().text("&eUsage: /coalgov admin tax exempt <player> <on|off|status>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void grant(CommandSender sender, OfflinePlayer target, String[] args) throws SQLException {
|
||||||
|
if (args.length != 4) {
|
||||||
|
sender.sendMessage(plugin.messages().text("&eUsage: /coalgov admin grant <player> <amount>"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long amount = CoalMoney.parsePositive(args[3]);
|
||||||
|
if (amount <= 0) {
|
||||||
|
sender.sendMessage(plugin.messages().text("&cAmount must be greater than zero."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
plugin.economyService().grant(target.getUniqueId(), amount);
|
||||||
|
sender.sendMessage(plugin.messages().text("&aGranted &f" + CoalMoney.format(amount) + "&a."));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void take(CommandSender sender, OfflinePlayer target, String[] args) throws SQLException {
|
||||||
|
if (args.length != 4) {
|
||||||
|
sender.sendMessage(plugin.messages().text("&eUsage: /coalgov admin take <player> <amount>"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long amount = CoalMoney.parsePositive(args[3]);
|
||||||
|
if (amount <= 0) {
|
||||||
|
sender.sendMessage(plugin.messages().text("&cAmount must be greater than zero."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (plugin.economyService().take(target.getUniqueId(), amount)) {
|
||||||
|
sender.sendMessage(plugin.messages().text("&aTook &f" + CoalMoney.format(amount) + "&a."));
|
||||||
|
} else {
|
||||||
|
sender.sendMessage(plugin.messages().text("&cBalance too low."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,298 @@
|
|||||||
|
package com.librewiki.coalgov.command;
|
||||||
|
|
||||||
|
import com.librewiki.coalgov.CoalGovPlugin;
|
||||||
|
import com.librewiki.coalgov.model.Claim;
|
||||||
|
import com.librewiki.coalgov.model.ClaimPoint;
|
||||||
|
import com.librewiki.coalgov.model.LandRegion;
|
||||||
|
import com.librewiki.coalgov.service.ClaimService;
|
||||||
|
import com.librewiki.coalgov.service.LandAppraisalService;
|
||||||
|
import com.librewiki.coalgov.util.CoalMoney;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.OfflinePlayer;
|
||||||
|
import org.bukkit.command.Command;
|
||||||
|
import org.bukkit.command.CommandExecutor;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public final class ClaimCommand implements CommandExecutor {
|
||||||
|
private final CoalGovPlugin plugin;
|
||||||
|
|
||||||
|
public ClaimCommand(CoalGovPlugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||||
|
if (!(sender instanceof Player player)) {
|
||||||
|
sender.sendMessage("Players only.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (args.length == 0) {
|
||||||
|
player.sendMessage(plugin.messages().text("&eUsage: /claim <wand|buy|info|list|show|appraise|sell|transfer|abandon>"));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
plugin.economyService().ensurePlayer(player);
|
||||||
|
switch (args[0].toLowerCase()) {
|
||||||
|
case "wand" -> wand(player);
|
||||||
|
case "buy" -> buy(player, args);
|
||||||
|
case "info" -> info(player);
|
||||||
|
case "list" -> list(player);
|
||||||
|
case "show" -> show(player, args);
|
||||||
|
case "appraise" -> appraise(player, args);
|
||||||
|
case "sell" -> sell(player, args);
|
||||||
|
case "transfer" -> transfer(player, args);
|
||||||
|
case "abandon" -> abandon(player, args);
|
||||||
|
default -> player.sendMessage(plugin.messages().text("&eUsage: /claim <wand|buy|info|list|show|appraise|sell|transfer|abandon>"));
|
||||||
|
}
|
||||||
|
} catch (SQLException exception) {
|
||||||
|
plugin.getLogger().warning("Claim command failed: " + exception.getMessage());
|
||||||
|
player.sendMessage(plugin.messages().text("&cClaim registry unavailable."));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buy(Player player, String[] args) throws SQLException {
|
||||||
|
if (args.length != 2 && args.length != 3) {
|
||||||
|
player.sendMessage(plugin.messages().text("&eUsage: /claim buy <homestead|industrial> [radius]"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClaimService.BuyResult result;
|
||||||
|
if (args.length == 3) {
|
||||||
|
int radius = parseInt(args[2]);
|
||||||
|
result = plugin.claimService().buy(player.getUniqueId(), player.getLocation(), args[1], radius);
|
||||||
|
} else {
|
||||||
|
List<ClaimPoint> points = plugin.claimSelectionService().claimPoints(player);
|
||||||
|
result = plugin.claimService().buyPolygon(player.getUniqueId(), player.getWorld().getName(), args[1], points);
|
||||||
|
}
|
||||||
|
if (!result.success()) {
|
||||||
|
player.sendMessage(plugin.messages().text("&c" + result.message()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
player.sendMessage(plugin.messages().text("&aClaim #" + result.id() + " created for &f" + CoalMoney.format(result.cost()) + "&a."));
|
||||||
|
plugin.claimSelectionService().consume(player);
|
||||||
|
plugin.claimService().findOwnedById(player.getUniqueId(), result.id()).ifPresent(claim -> {
|
||||||
|
plugin.claimMarkerService().placeMarkers(claim);
|
||||||
|
plugin.claimVisualService().show(player, claim);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void wand(Player player) {
|
||||||
|
player.getInventory().addItem(plugin.claimSelectionService().wand());
|
||||||
|
player.sendMessage(plugin.messages().text("&aClaim wand issued. Right-click blocks to draw a polygon with marker previews; sneak right-click to clear."));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void info(Player player) throws SQLException {
|
||||||
|
Optional<Claim> claim = plugin.claimService().claimAt(player.getLocation());
|
||||||
|
Optional<LandRegion> region = plugin.landService().regionAt(player.getLocation());
|
||||||
|
player.sendMessage(plugin.messages().text("&eLand: &f" + region.map(r -> r.name() + " " + r.landClass()).orElse(plugin.landService().classAt(player.getLocation()).name())));
|
||||||
|
if (claim.isPresent()) {
|
||||||
|
Claim c = claim.get();
|
||||||
|
player.sendMessage(plugin.messages().text("&eClaim: &f#" + c.id() + " " + c.claimType() + " owner " + c.ownerUuid()));
|
||||||
|
if (c.isPolygon()) {
|
||||||
|
player.sendMessage(plugin.messages().text("&eShape: &fpolygon with " + c.vertices().size() + " points"));
|
||||||
|
}
|
||||||
|
plugin.claimVisualService().show(player, c);
|
||||||
|
} else {
|
||||||
|
player.sendMessage(plugin.messages().text("&eClaim: &fnone"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void list(Player player) throws SQLException {
|
||||||
|
List<Claim> claims = plugin.claimService().list(player.getUniqueId());
|
||||||
|
if (claims.isEmpty()) {
|
||||||
|
player.sendMessage(plugin.messages().text("&eYou have no claims."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
player.sendMessage(plugin.messages().text("&eYour claims:"));
|
||||||
|
for (Claim claim : claims) {
|
||||||
|
String shape = claim.isPolygon() ? " polygon" : " rectangle";
|
||||||
|
player.sendMessage(plugin.messages().raw("&7#" + claim.id() + " &f" + claim.claimType() + shape + " purchase " + CoalMoney.format(plugin.claimService().claimPurchaseCost(claim)) + " " + claim.world()
|
||||||
|
+ " [" + claim.x1() + "," + claim.z1() + "] to [" + claim.x2() + "," + claim.z2() + "]"));
|
||||||
|
}
|
||||||
|
player.sendMessage(plugin.messages().text("&7Use &f/claim appraise [id] &7for sale value, or &f/claim show <id>&7."));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void show(Player player, String[] args) throws SQLException {
|
||||||
|
if (args.length != 2) {
|
||||||
|
player.sendMessage(plugin.messages().text("&eUsage: /claim show <id>"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long id = parseLong(args[1]);
|
||||||
|
if (id <= 0) {
|
||||||
|
player.sendMessage(plugin.messages().text("&cInvalid claim id."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Optional<Claim> claim = plugin.claimService().findOwnedById(player.getUniqueId(), id);
|
||||||
|
if (claim.isEmpty()) {
|
||||||
|
player.sendMessage(plugin.messages().text("&cNo owned claim with that id."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
plugin.claimVisualService().show(player, claim.get());
|
||||||
|
player.sendMessage(plugin.messages().text("&aShowing claim #" + id + " boundary."));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appraise(Player player, String[] args) throws SQLException {
|
||||||
|
Optional<Claim> claim = resolveOwnedClaim(player, args, "appraise");
|
||||||
|
if (claim.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
LandAppraisalService.Appraisal appraisal = plugin.landAppraisalService().appraise(claim.get());
|
||||||
|
if (!appraisal.worldLoaded()) {
|
||||||
|
player.sendMessage(plugin.messages().text("&cThat claim's world is not loaded."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sendAppraisal(player, appraisal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sell(Player player, String[] args) throws SQLException {
|
||||||
|
Optional<Claim> claim = resolveOwnedClaim(player, args, "sell");
|
||||||
|
if (claim.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LandAppraisalService.Appraisal appraisal = plugin.landAppraisalService().appraise(claim.get());
|
||||||
|
if (!appraisal.worldLoaded()) {
|
||||||
|
player.sendMessage(plugin.messages().text("&cThat claim's world is not loaded."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ClaimService.SellResult result = plugin.claimService().sell(player.getUniqueId(), claim.get().id(), appraisal.totalValue());
|
||||||
|
if (!result.success()) {
|
||||||
|
player.sendMessage(plugin.messages().text("&c" + result.message()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
plugin.claimMarkerService().removeMarkers(result.claim());
|
||||||
|
if (result.refund() >= 0L) {
|
||||||
|
player.sendMessage(plugin.messages().text("&aSold claim #" + result.claim().id() + " back to the Ministry for &f" + CoalMoney.format(result.refund()) + "&a."));
|
||||||
|
} else {
|
||||||
|
player.sendMessage(plugin.messages().text("&eSurrendered claim #" + result.claim().id() + " and paid &f" + CoalMoney.format(Math.abs(result.refund())) + " &efor extracted land value."));
|
||||||
|
}
|
||||||
|
sendAppraisal(player, appraisal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void transfer(Player player, String[] args) throws SQLException {
|
||||||
|
if (args.length != 3) {
|
||||||
|
player.sendMessage(plugin.messages().text("&eUsage: /claim transfer <id> <player>"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long id = parseLong(args[1]);
|
||||||
|
if (id <= 0L) {
|
||||||
|
player.sendMessage(plugin.messages().text("&cInvalid claim id."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
OfflinePlayer recipient = Bukkit.getOfflinePlayer(args[2]);
|
||||||
|
plugin.economyService().ensurePlayer(recipient);
|
||||||
|
ClaimService.TransferResult result = plugin.claimService().transfer(player.getUniqueId(), id, recipient.getUniqueId());
|
||||||
|
if (!result.success()) {
|
||||||
|
player.sendMessage(plugin.messages().text("&c" + result.message()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String recipientName = recipient.getName() == null ? recipient.getUniqueId().toString() : recipient.getName();
|
||||||
|
player.sendMessage(plugin.messages().text("&aTransferred claim #" + id + " to &f" + recipientName + "&a."));
|
||||||
|
if (recipient.isOnline() && recipient.getPlayer() != null) {
|
||||||
|
recipient.getPlayer().sendMessage(plugin.messages().text("&a" + player.getName() + " transferred claim #" + id + " to you."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<Claim> resolveOwnedClaim(Player player, String[] args, String action) throws SQLException {
|
||||||
|
if (args.length > 2) {
|
||||||
|
player.sendMessage(plugin.messages().text("&eUsage: /claim " + action + " [id]"));
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<Claim> claim;
|
||||||
|
if (args.length == 2) {
|
||||||
|
long id = parseLong(args[1]);
|
||||||
|
if (id <= 0) {
|
||||||
|
player.sendMessage(plugin.messages().text("&cInvalid claim id."));
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
claim = plugin.claimService().findOwnedById(player.getUniqueId(), id);
|
||||||
|
} else {
|
||||||
|
claim = plugin.claimService().claimAt(player.getLocation())
|
||||||
|
.filter(c -> c.ownerUuid().equals(player.getUniqueId()));
|
||||||
|
}
|
||||||
|
if (claim.isEmpty()) {
|
||||||
|
player.sendMessage(plugin.messages().text("&cStand inside one of your claims, or use /claim " + action + " <id>."));
|
||||||
|
}
|
||||||
|
return claim;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendAppraisal(Player player, LandAppraisalService.Appraisal appraisal) {
|
||||||
|
player.sendMessage(plugin.messages().text("&eAppraisal: &f" + CoalMoney.format(appraisal.totalValue())));
|
||||||
|
player.sendMessage(plugin.messages().raw("&7Land condition: &f" + appraisal.conditionPercent()
|
||||||
|
+ "% &7base value: &f" + CoalMoney.format(Math.round(appraisal.conditionValue()))
|
||||||
|
+ " &7resources: &f" + CoalMoney.format(appraisal.resourceValue())
|
||||||
|
+ " &7depletion charge: &f" + CoalMoney.format(appraisal.depletionChargeRounded())));
|
||||||
|
if (appraisal.originalCost() == 0L) {
|
||||||
|
player.sendMessage(plugin.messages().raw("&7Starter homestead land value is &f0&7; only resources can offset depletion."));
|
||||||
|
}
|
||||||
|
String resourceSummary = resourceSummary(appraisal);
|
||||||
|
if (!resourceSummary.isEmpty()) {
|
||||||
|
player.sendMessage(plugin.messages().raw("&7Counted resources: &f" + resourceSummary));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resourceSummary(LandAppraisalService.Appraisal appraisal) {
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
int shown = 0;
|
||||||
|
for (var entry : appraisal.resources().entrySet()) {
|
||||||
|
if (entry.getValue() <= 0L) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (shown++ > 0) {
|
||||||
|
builder.append(", ");
|
||||||
|
}
|
||||||
|
builder.append(readableMaterial(entry.getKey())).append(" ").append(entry.getValue());
|
||||||
|
if (shown >= 5) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String readableMaterial(Material material) {
|
||||||
|
return material.name().toLowerCase().replace('_', ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
private void abandon(Player player, String[] args) throws SQLException {
|
||||||
|
if (args.length != 2) {
|
||||||
|
player.sendMessage(plugin.messages().text("&eUsage: /claim abandon <id>"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long id = parseLong(args[1]);
|
||||||
|
if (id <= 0) {
|
||||||
|
player.sendMessage(plugin.messages().text("&cInvalid claim id."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Optional<Claim> claim = plugin.claimService().findOwnedById(player.getUniqueId(), id);
|
||||||
|
if (plugin.claimService().abandon(player.getUniqueId(), id)) {
|
||||||
|
claim.ifPresent(c -> plugin.claimMarkerService().removeMarkers(c));
|
||||||
|
player.sendMessage(plugin.messages().text("&aClaim abandoned."));
|
||||||
|
} else {
|
||||||
|
player.sendMessage(plugin.messages().text("&cNo owned claim with that id."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int parseInt(String raw) {
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(raw);
|
||||||
|
} catch (NumberFormatException exception) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private long parseLong(String raw) {
|
||||||
|
try {
|
||||||
|
return Long.parseLong(raw);
|
||||||
|
} catch (NumberFormatException exception) {
|
||||||
|
return -1L;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
package com.librewiki.coalgov.command;
|
||||||
|
|
||||||
|
import com.librewiki.coalgov.CoalGovPlugin;
|
||||||
|
import com.librewiki.coalgov.model.Fine;
|
||||||
|
import com.librewiki.coalgov.util.CoalMoney;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.command.Command;
|
||||||
|
import org.bukkit.command.CommandExecutor;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class CoalCommand implements CommandExecutor {
|
||||||
|
private final CoalGovPlugin plugin;
|
||||||
|
|
||||||
|
public CoalCommand(CoalGovPlugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||||
|
if (!(sender instanceof Player player)) {
|
||||||
|
sender.sendMessage("Players only.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (args.length == 0) {
|
||||||
|
player.sendMessage(plugin.messages().text("&eUsage: /coal <balance|deposit|withdraw|pay|fines>"));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
plugin.economyService().ensurePlayer(player);
|
||||||
|
switch (args[0].toLowerCase()) {
|
||||||
|
case "balance" -> balance(player);
|
||||||
|
case "deposit" -> deposit(player);
|
||||||
|
case "withdraw" -> withdraw(player, args);
|
||||||
|
case "pay" -> pay(player, args);
|
||||||
|
case "fines" -> fines(player, args);
|
||||||
|
default -> player.sendMessage(plugin.messages().text("&eUsage: /coal <balance|deposit|withdraw|pay|fines>"));
|
||||||
|
}
|
||||||
|
} catch (SQLException exception) {
|
||||||
|
plugin.getLogger().warning("Coal command failed: " + exception.getMessage());
|
||||||
|
player.sendMessage(plugin.messages().text("&cCoal ledger unavailable."));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void balance(Player player) throws SQLException {
|
||||||
|
player.sendMessage(plugin.messages().text("&eBalance: &f" + CoalMoney.format(plugin.economyService().balance(player.getUniqueId()))));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deposit(Player player) throws SQLException {
|
||||||
|
boolean coalEnabled = plugin.getConfig().getBoolean("economy.deposit_coal", true);
|
||||||
|
boolean blocksEnabled = plugin.getConfig().getBoolean("economy.deposit_coal_blocks", true);
|
||||||
|
long credit = 0;
|
||||||
|
for (ItemStack item : player.getInventory().getContents()) {
|
||||||
|
if (item == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (coalEnabled && item.getType() == Material.COAL) {
|
||||||
|
credit += item.getAmount() * CoalMoney.CENTS_PER_COAL;
|
||||||
|
item.setAmount(0);
|
||||||
|
} else if (blocksEnabled && item.getType() == Material.COAL_BLOCK) {
|
||||||
|
credit += item.getAmount() * 9L * CoalMoney.CENTS_PER_COAL;
|
||||||
|
item.setAmount(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (credit <= 0) {
|
||||||
|
player.sendMessage(plugin.messages().text("&cNo depositable coal found."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
plugin.economyService().deposit(player.getUniqueId(), credit);
|
||||||
|
player.sendMessage(plugin.messages().text("&aDeposited &f" + CoalMoney.format(credit) + "&a."));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void withdraw(Player player, String[] args) throws SQLException {
|
||||||
|
if (args.length != 2) {
|
||||||
|
player.sendMessage(plugin.messages().text("&eUsage: /coal withdraw <amount>"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long amount = CoalMoney.parsePositive(args[1]);
|
||||||
|
if (amount <= 0) {
|
||||||
|
player.sendMessage(plugin.messages().text("&cAmount must be greater than zero."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (amount % CoalMoney.CENTS_PER_COAL != 0L) {
|
||||||
|
player.sendMessage(plugin.messages().text("&cWithdrawals must be whole coal because they create coal items."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long coalItems = amount / CoalMoney.CENTS_PER_COAL;
|
||||||
|
if (plugin.economyService().balance(player.getUniqueId()) < amount) {
|
||||||
|
player.sendMessage(plugin.messages().text("&cBalance too low."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!canFitCoal(player, coalItems)) {
|
||||||
|
player.sendMessage(plugin.messages().text("&cYou need more inventory space."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!plugin.economyService().withdraw(player.getUniqueId(), amount)) {
|
||||||
|
player.sendMessage(plugin.messages().text("&cBalance too low."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long remaining = coalItems;
|
||||||
|
while (remaining > 0) {
|
||||||
|
int stack = (int) Math.min(Material.COAL.getMaxStackSize(), remaining);
|
||||||
|
player.getInventory().addItem(new ItemStack(Material.COAL, stack));
|
||||||
|
remaining -= stack;
|
||||||
|
}
|
||||||
|
player.sendMessage(plugin.messages().text("&aWithdrew &f" + CoalMoney.format(amount) + "&a."));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pay(Player player, String[] args) throws SQLException {
|
||||||
|
if (args.length != 3) {
|
||||||
|
player.sendMessage(plugin.messages().text("&eUsage: /coal pay <player> <amount>"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Player target = Bukkit.getPlayerExact(args[1]);
|
||||||
|
if (target == null) {
|
||||||
|
player.sendMessage(plugin.messages().text("&cThat player must be online."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long amount = CoalMoney.parsePositive(args[2]);
|
||||||
|
if (amount <= 0) {
|
||||||
|
player.sendMessage(plugin.messages().text("&cAmount must be greater than zero."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
plugin.economyService().ensurePlayer(target);
|
||||||
|
if (!plugin.economyService().pay(player.getUniqueId(), target.getUniqueId(), amount)) {
|
||||||
|
player.sendMessage(plugin.messages().text("&cPayment failed. Check your balance."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
player.sendMessage(plugin.messages().text("&aPaid &f" + CoalMoney.format(amount) + " &ato " + target.getName() + "."));
|
||||||
|
target.sendMessage(plugin.messages().text("&aReceived &f" + CoalMoney.format(amount) + " &afrom " + player.getName() + "."));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fines(Player player, String[] args) throws SQLException {
|
||||||
|
if (args.length == 1 || (args.length == 2 && args[1].equalsIgnoreCase("list"))) {
|
||||||
|
List<Fine> fines = plugin.governmentService().unpaidFines(player.getUniqueId());
|
||||||
|
if (fines.isEmpty()) {
|
||||||
|
player.sendMessage(plugin.messages().text("&aYou have no unpaid fines."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long total = fines.stream().mapToLong(Fine::amount).sum();
|
||||||
|
player.sendMessage(plugin.messages().text("&eUnpaid fines: &f" + CoalMoney.format(total) + " &etotal"));
|
||||||
|
for (Fine fine : fines) {
|
||||||
|
player.sendMessage(plugin.messages().raw("&7#" + fine.id() + " &f" + CoalMoney.format(fine.amount()) + " &7" + fine.reason()));
|
||||||
|
}
|
||||||
|
player.sendMessage(plugin.messages().text("&7Use &f/coal fines pay <id|all>&7."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (args.length == 3 && args[1].equalsIgnoreCase("pay")) {
|
||||||
|
if (args[2].equalsIgnoreCase("all")) {
|
||||||
|
var result = plugin.governmentService().payAllFines(player.getUniqueId());
|
||||||
|
if (!result.success()) {
|
||||||
|
player.sendMessage(plugin.messages().text("&c" + result.message()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
player.sendMessage(plugin.messages().text("&aPaid &f" + CoalMoney.format(result.amount()) + " &ain fines."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long id = parsePositive(args[2]);
|
||||||
|
if (id <= 0L) {
|
||||||
|
player.sendMessage(plugin.messages().text("&cInvalid fine id."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var result = plugin.governmentService().payFine(player.getUniqueId(), id);
|
||||||
|
if (!result.success()) {
|
||||||
|
player.sendMessage(plugin.messages().text("&c" + result.message()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
player.sendMessage(plugin.messages().text("&aPaid fine #" + id + " for &f" + CoalMoney.format(result.amount()) + "&a."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
player.sendMessage(plugin.messages().text("&eUsage: /coal fines [list|pay <id|all>]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean canFitCoal(Player player, long amount) {
|
||||||
|
if (amount > Integer.MAX_VALUE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
long capacity = 0;
|
||||||
|
for (ItemStack item : player.getInventory().getStorageContents()) {
|
||||||
|
if (item == null || item.getType().isAir()) {
|
||||||
|
capacity += Material.COAL.getMaxStackSize();
|
||||||
|
} else if (item.getType() == Material.COAL) {
|
||||||
|
capacity += Material.COAL.getMaxStackSize() - item.getAmount();
|
||||||
|
}
|
||||||
|
if (capacity >= amount) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long parsePositive(String raw) {
|
||||||
|
try {
|
||||||
|
return Long.parseLong(raw);
|
||||||
|
} catch (NumberFormatException exception) {
|
||||||
|
return -1L;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.librewiki.coalgov.command;
|
||||||
|
|
||||||
|
import com.librewiki.coalgov.CoalGovPlugin;
|
||||||
|
import org.bukkit.command.Command;
|
||||||
|
import org.bukkit.command.CommandExecutor;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
public final class DiviningRodCommand implements CommandExecutor {
|
||||||
|
private final CoalGovPlugin plugin;
|
||||||
|
|
||||||
|
public DiviningRodCommand(CoalGovPlugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||||
|
if (!(sender instanceof Player player)) {
|
||||||
|
sender.sendMessage("Players only.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (args.length != 0) {
|
||||||
|
player.sendMessage(plugin.messages().text("&eUsage: /diviningrod"));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
player.getInventory().addItem(plugin.diviningRodService().rod());
|
||||||
|
player.sendMessage(plugin.messages().text("&aDivining rod granted."));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
package com.librewiki.coalgov.command;
|
||||||
|
|
||||||
|
import com.librewiki.coalgov.CoalGovPlugin;
|
||||||
|
import com.librewiki.coalgov.model.LandClass;
|
||||||
|
import com.librewiki.coalgov.model.LandRegion;
|
||||||
|
import com.librewiki.coalgov.util.Cuboid2D;
|
||||||
|
import org.bukkit.command.Command;
|
||||||
|
import org.bukkit.command.CommandExecutor;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public final class LandCommand implements CommandExecutor {
|
||||||
|
private final CoalGovPlugin plugin;
|
||||||
|
|
||||||
|
public LandCommand(CoalGovPlugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||||
|
if (args.length == 0) {
|
||||||
|
sender.sendMessage(plugin.messages().text("&eUsage: /land <info|create|delete|list>"));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
switch (args[0].toLowerCase()) {
|
||||||
|
case "info" -> info(sender);
|
||||||
|
case "create" -> create(sender, args);
|
||||||
|
case "delete" -> delete(sender, args);
|
||||||
|
case "list" -> list(sender);
|
||||||
|
default -> sender.sendMessage(plugin.messages().text("&eUsage: /land <info|create|delete|list>"));
|
||||||
|
}
|
||||||
|
} catch (SQLException exception) {
|
||||||
|
plugin.getLogger().warning("Land command failed: " + exception.getMessage());
|
||||||
|
sender.sendMessage(plugin.messages().text("&cLand registry unavailable."));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void info(CommandSender sender) throws SQLException {
|
||||||
|
if (!(sender instanceof Player player)) {
|
||||||
|
sender.sendMessage("Players only.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!player.hasPermission("coalgov.land.info") && !player.hasPermission("coalgov.admin")) {
|
||||||
|
player.sendMessage(plugin.messages().text("&cYou lack permission."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Optional<LandRegion> region = plugin.landService().regionAt(player.getLocation());
|
||||||
|
if (region.isPresent()) {
|
||||||
|
LandRegion r = region.get();
|
||||||
|
player.sendMessage(plugin.messages().text("&eLand: &f" + r.name() + " " + r.landClass()
|
||||||
|
+ " [" + r.x1() + "," + r.z1() + "] to [" + r.x2() + "," + r.z2() + "]"));
|
||||||
|
} else {
|
||||||
|
player.sendMessage(plugin.messages().text("&eLand: &f" + plugin.landService().classAt(player.getLocation()) + " default"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void create(CommandSender sender, String[] args) throws SQLException {
|
||||||
|
if (!(sender instanceof Player player)) {
|
||||||
|
sender.sendMessage("Players only.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!player.hasPermission("coalgov.admin")) {
|
||||||
|
player.sendMessage(plugin.messages().text("&cYou lack permission."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (args.length != 4) {
|
||||||
|
player.sendMessage(plugin.messages().text("&eUsage: /land create <name> <landClass> <radius>"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
LandClass landClass;
|
||||||
|
try {
|
||||||
|
landClass = LandClass.valueOf(args[2].toUpperCase());
|
||||||
|
} catch (IllegalArgumentException exception) {
|
||||||
|
player.sendMessage(plugin.messages().text("&cInvalid land class."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int radius = parseInt(args[3]);
|
||||||
|
if (radius <= 0) {
|
||||||
|
player.sendMessage(plugin.messages().text("&cRadius must be greater than zero."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Cuboid2D area = new Cuboid2D(
|
||||||
|
player.getWorld().getName(),
|
||||||
|
player.getLocation().getBlockX() - radius,
|
||||||
|
player.getLocation().getBlockZ() - radius,
|
||||||
|
player.getLocation().getBlockX() + radius,
|
||||||
|
player.getLocation().getBlockZ() + radius
|
||||||
|
);
|
||||||
|
if (plugin.landService().create(args[1], area, landClass)) {
|
||||||
|
player.sendMessage(plugin.messages().text("&aLand region created."));
|
||||||
|
} else {
|
||||||
|
player.sendMessage(plugin.messages().text("&cCould not create land region."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void delete(CommandSender sender, String[] args) throws SQLException {
|
||||||
|
if (!sender.hasPermission("coalgov.admin")) {
|
||||||
|
sender.sendMessage(plugin.messages().text("&cYou lack permission."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (args.length != 2) {
|
||||||
|
sender.sendMessage(plugin.messages().text("&eUsage: /land delete <name>"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (plugin.landService().delete(args[1])) {
|
||||||
|
sender.sendMessage(plugin.messages().text("&aLand region deleted."));
|
||||||
|
} else {
|
||||||
|
sender.sendMessage(plugin.messages().text("&cNo land region with that name."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void list(CommandSender sender) throws SQLException {
|
||||||
|
List<LandRegion> regions = plugin.landService().listAll();
|
||||||
|
if (regions.isEmpty()) {
|
||||||
|
sender.sendMessage(plugin.messages().text("&eNo land regions."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sender.sendMessage(plugin.messages().text("&eLand regions:"));
|
||||||
|
for (LandRegion r : regions) {
|
||||||
|
sender.sendMessage(plugin.messages().raw("&7" + r.name() + " &f" + r.landClass() + " " + r.world()
|
||||||
|
+ " [" + r.x1() + "," + r.z1() + "] to [" + r.x2() + "," + r.z2() + "]"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int parseInt(String raw) {
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(raw);
|
||||||
|
} catch (NumberFormatException exception) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.librewiki.coalgov.command;
|
||||||
|
|
||||||
|
import com.librewiki.coalgov.CoalGovPlugin;
|
||||||
|
import org.bukkit.command.Command;
|
||||||
|
import org.bukkit.command.CommandExecutor;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
public final class MarketCommand implements CommandExecutor {
|
||||||
|
private final CoalGovPlugin plugin;
|
||||||
|
|
||||||
|
public MarketCommand(CoalGovPlugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||||
|
if (!(sender instanceof Player player)) {
|
||||||
|
sender.sendMessage("Players only.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
plugin.economyService().ensurePlayer(player);
|
||||||
|
plugin.marketService().open(player);
|
||||||
|
} catch (SQLException exception) {
|
||||||
|
plugin.getLogger().warning("Market command failed: " + exception.getMessage());
|
||||||
|
player.sendMessage(plugin.messages().text("&cMarket ledger unavailable."));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
package com.librewiki.coalgov.command;
|
||||||
|
|
||||||
|
import com.librewiki.coalgov.CoalGovPlugin;
|
||||||
|
import com.librewiki.coalgov.model.ClaimPoint;
|
||||||
|
import com.librewiki.coalgov.model.CoalNpc;
|
||||||
|
import com.librewiki.coalgov.model.NpcZone;
|
||||||
|
import com.librewiki.coalgov.service.NpcService;
|
||||||
|
import com.librewiki.coalgov.service.OpenRouterService;
|
||||||
|
import com.librewiki.coalgov.util.CoalMoney;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.command.Command;
|
||||||
|
import org.bukkit.command.CommandExecutor;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public final class NpcCommand implements CommandExecutor {
|
||||||
|
private final CoalGovPlugin plugin;
|
||||||
|
|
||||||
|
public NpcCommand(CoalGovPlugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||||
|
if (!sender.hasPermission("coalgov.admin")) {
|
||||||
|
sender.sendMessage(plugin.messages().text("&cYou lack permission."));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (args.length >= 1 && args[0].equalsIgnoreCase("zone")) {
|
||||||
|
zone(sender, args);
|
||||||
|
} else if (args.length >= 1 && args[0].equalsIgnoreCase("create")) {
|
||||||
|
create(sender, args);
|
||||||
|
} else if (args.length >= 1 && args[0].equalsIgnoreCase("remove")) {
|
||||||
|
remove(sender, args);
|
||||||
|
} else if (args.length >= 1 && args[0].equalsIgnoreCase("list")) {
|
||||||
|
list(sender);
|
||||||
|
} else if (args.length >= 1 && args[0].equalsIgnoreCase("stock")) {
|
||||||
|
stock(sender, args);
|
||||||
|
} else if (args.length >= 1 && args[0].equalsIgnoreCase("funds")) {
|
||||||
|
funds(sender, args);
|
||||||
|
} else if (args.length >= 1 && args[0].equalsIgnoreCase("haggle")) {
|
||||||
|
haggle(sender, args);
|
||||||
|
} else {
|
||||||
|
usage(sender);
|
||||||
|
}
|
||||||
|
} catch (SQLException exception) {
|
||||||
|
plugin.getLogger().warning("NPC command failed: " + exception.getMessage());
|
||||||
|
sender.sendMessage(plugin.messages().text("&cNPC registry unavailable."));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void zone(CommandSender sender, String[] args) throws SQLException {
|
||||||
|
if (!(sender instanceof Player player)) {
|
||||||
|
sender.sendMessage("Players only.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (args.length != 3 || !args[1].equalsIgnoreCase("create")) {
|
||||||
|
player.sendMessage(plugin.messages().text("&eUsage: /cgnpc zone create <name>"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<ClaimPoint> points = plugin.claimSelectionService().claimPoints(player);
|
||||||
|
if (points.size() < 3) {
|
||||||
|
player.sendMessage(plugin.messages().text("&cUse /claim wand to select at least 3 polygon points."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long id = plugin.npcService().createZone(args[2], player.getWorld().getName(), points);
|
||||||
|
plugin.claimSelectionService().consume(player);
|
||||||
|
player.sendMessage(plugin.messages().text("&aNPC zone #" + id + " created."));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void create(CommandSender sender, String[] args) throws SQLException {
|
||||||
|
if (!(sender instanceof Player player)) {
|
||||||
|
sender.sendMessage("Players only.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (args.length != 4) {
|
||||||
|
player.sendMessage(plugin.messages().text("&eUsage: /cgnpc create <trader|worker> <name> <zone>"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String type = args[1].toUpperCase();
|
||||||
|
if (!type.equals("TRADER") && !type.equals("WORKER")) {
|
||||||
|
player.sendMessage(plugin.messages().text("&cNPC type must be trader or worker."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Optional<NpcZone> zone = plugin.npcService().findZone(args[3]);
|
||||||
|
if (zone.isEmpty()) {
|
||||||
|
player.sendMessage(plugin.messages().text("&cNo NPC zone named " + args[3] + "."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long id = plugin.npcService().createNpc(type, args[2], zone.get(), player.getLocation());
|
||||||
|
player.sendMessage(plugin.messages().text("&aNPC #" + id + " created."));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void remove(CommandSender sender, String[] args) throws SQLException {
|
||||||
|
if (args.length != 2) {
|
||||||
|
sender.sendMessage(plugin.messages().text("&eUsage: /cgnpc remove <id>"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long id = parseLong(args[1]);
|
||||||
|
if (plugin.npcService().removeNpc(id)) {
|
||||||
|
sender.sendMessage(plugin.messages().text("&aNPC removed."));
|
||||||
|
} else {
|
||||||
|
sender.sendMessage(plugin.messages().text("&cNo NPC with that id."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void list(CommandSender sender) throws SQLException {
|
||||||
|
sender.sendMessage(plugin.messages().text("&eNPC zones:"));
|
||||||
|
for (NpcZone zone : plugin.npcService().listZones()) {
|
||||||
|
sender.sendMessage(plugin.messages().raw("&7#" + zone.id() + " &f" + zone.name() + " " + zone.world()));
|
||||||
|
}
|
||||||
|
sender.sendMessage(plugin.messages().text("&eNPCs:"));
|
||||||
|
for (CoalNpc npc : plugin.npcService().listNpcs()) {
|
||||||
|
sender.sendMessage(plugin.messages().raw("&7#" + npc.id() + " &f" + npc.name() + " " + npc.npcType() + " citizens " + npc.citizensId()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stock(CommandSender sender, String[] args) throws SQLException {
|
||||||
|
if (args.length != 4) {
|
||||||
|
sender.sendMessage(plugin.messages().text("&eUsage: /cgnpc stock <id> <material> <amount>"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Optional<CoalNpc> npc = plugin.npcService().findNpc(parseLong(args[1]));
|
||||||
|
Material material = Material.matchMaterial(args[2]);
|
||||||
|
long amount = parseLong(args[3]);
|
||||||
|
if (npc.isEmpty() || material == null || amount <= 0L) {
|
||||||
|
sender.sendMessage(plugin.messages().text("&cInvalid NPC, material, or amount."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
plugin.npcService().addInventory(npc.get().id(), material, amount);
|
||||||
|
sender.sendMessage(plugin.messages().text("&aNPC stock added."));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void funds(CommandSender sender, String[] args) throws SQLException {
|
||||||
|
if (args.length != 3) {
|
||||||
|
sender.sendMessage(plugin.messages().text("&eUsage: /cgnpc funds <id> <amount>"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Optional<CoalNpc> npc = plugin.npcService().findNpc(parseLong(args[1]));
|
||||||
|
long amount = CoalMoney.parsePositive(args[2]);
|
||||||
|
if (npc.isEmpty() || amount <= 0L) {
|
||||||
|
sender.sendMessage(plugin.messages().text("&cInvalid NPC or amount."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
plugin.economyService().credit(npc.get().accountUuid(), amount, "npc_admin_funds");
|
||||||
|
sender.sendMessage(plugin.messages().text("&aNPC funded."));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void haggle(CommandSender sender, String[] args) throws SQLException {
|
||||||
|
if (!(sender instanceof Player player)) {
|
||||||
|
sender.sendMessage("Players only.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (args.length != 6) {
|
||||||
|
player.sendMessage(plugin.messages().text("&eUsage: /cgnpc haggle <id> <buy|sell> <material> <amount> <offer>"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Optional<CoalNpc> npc = plugin.npcService().findNpc(parseLong(args[1]));
|
||||||
|
Material material = Material.matchMaterial(args[3]);
|
||||||
|
int amount = (int) parseLong(args[4]);
|
||||||
|
long offer = CoalMoney.parsePositive(args[5]);
|
||||||
|
if (npc.isEmpty() || !npc.get().trader() || material == null || amount <= 0 || offer <= 0L) {
|
||||||
|
player.sendMessage(plugin.messages().text("&cInvalid trader, material, amount, or offer."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NpcService.HaggleQuote quote = plugin.npcService().quote(npc.get(), args[2], material, amount, offer);
|
||||||
|
if (!quote.reason().isEmpty()) {
|
||||||
|
player.sendMessage(plugin.messages().text("&c" + quote.reason()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String prompt = "NPC " + npc.get().name() + " is haggling. Mode " + quote.mode()
|
||||||
|
+ ", material " + material.name() + ", amount " + amount
|
||||||
|
+ ", player offer " + CoalMoney.format(offer) + " (" + offer + " coal cents), acceptable range "
|
||||||
|
+ CoalMoney.format(quote.floor()) + " to " + CoalMoney.format(quote.ceiling()) + " (" + quote.floor() + " to " + quote.ceiling() + " coal cents)"
|
||||||
|
+ ", deterministic counter " + CoalMoney.format(quote.counter()) + " (" + quote.counter() + " coal cents). Reply with JSON using counter as an integer coal-cent amount.";
|
||||||
|
player.sendMessage(plugin.messages().text("&7" + npc.get().name() + " considers the offer..."));
|
||||||
|
plugin.openRouterService().haggle(prompt, quote.floor(), quote.ceiling()).thenAccept(ai -> plugin.getServer().getScheduler().runTask(plugin, () -> {
|
||||||
|
try {
|
||||||
|
long finalPrice = ai.map(OpenRouterService.HaggleAiResult::counter).orElse(quote.counter());
|
||||||
|
boolean accepted = quote.accepted() || ai.map(OpenRouterService.HaggleAiResult::accept).orElse(false) && offer == finalPrice;
|
||||||
|
String line = ai.map(OpenRouterService.HaggleAiResult::message)
|
||||||
|
.orElse(accepted ? "Deal." : "I can do " + CoalMoney.format(finalPrice) + ".");
|
||||||
|
player.sendMessage(plugin.messages().text("&e" + npc.get().name() + ": &f" + line));
|
||||||
|
if (accepted) {
|
||||||
|
if (plugin.npcService().completeTrade(player, npc.get(), quote, offer)) {
|
||||||
|
player.sendMessage(plugin.messages().text("&aTrade complete for &f" + CoalMoney.format(offer) + "&a."));
|
||||||
|
} else {
|
||||||
|
player.sendMessage(plugin.messages().text("&cTrade failed. Check balance, stock, and inventory."));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
player.sendMessage(plugin.messages().text("&7Counter offer: &f" + CoalMoney.format(finalPrice) + "&7. Run the haggle command again with that offer to accept."));
|
||||||
|
}
|
||||||
|
} catch (SQLException exception) {
|
||||||
|
player.sendMessage(plugin.messages().text("&cNPC trade failed."));
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void usage(CommandSender sender) {
|
||||||
|
sender.sendMessage(plugin.messages().text("&eUsage: /cgnpc <zone|create|remove|list|stock|funds|haggle>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private long parseLong(String raw) {
|
||||||
|
try {
|
||||||
|
return Long.parseLong(raw);
|
||||||
|
} catch (NumberFormatException exception) {
|
||||||
|
return -1L;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package com.librewiki.coalgov.command;
|
||||||
|
|
||||||
|
import com.librewiki.coalgov.CoalGovPlugin;
|
||||||
|
import com.librewiki.coalgov.model.Permit;
|
||||||
|
import com.librewiki.coalgov.service.PermitService;
|
||||||
|
import com.librewiki.coalgov.util.CoalMoney;
|
||||||
|
import org.bukkit.command.Command;
|
||||||
|
import org.bukkit.command.CommandExecutor;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class PermitCommand implements CommandExecutor {
|
||||||
|
private final CoalGovPlugin plugin;
|
||||||
|
|
||||||
|
public PermitCommand(CoalGovPlugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||||
|
if (!(sender instanceof Player player)) {
|
||||||
|
sender.sendMessage("Players only.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (args.length == 0) {
|
||||||
|
player.sendMessage(plugin.messages().text("&eUsage: /permit <buy|list>"));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
plugin.economyService().ensurePlayer(player);
|
||||||
|
switch (args[0].toLowerCase()) {
|
||||||
|
case "buy" -> buy(player, args);
|
||||||
|
case "list" -> list(player);
|
||||||
|
default -> player.sendMessage(plugin.messages().text("&eUsage: /permit <buy|list>"));
|
||||||
|
}
|
||||||
|
} catch (SQLException exception) {
|
||||||
|
plugin.getLogger().warning("Permit command failed: " + exception.getMessage());
|
||||||
|
player.sendMessage(plugin.messages().text("&cPermit registry unavailable."));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buy(Player player, String[] args) throws SQLException {
|
||||||
|
if (args.length != 3 || !args[1].equalsIgnoreCase("mining")) {
|
||||||
|
player.sendMessage(plugin.messages().text("&eUsage: /permit buy mining <regionName>"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PermitService.BuyResult result = plugin.permitService().buyMining(player.getUniqueId(), args[2]);
|
||||||
|
if (!result.success()) {
|
||||||
|
player.sendMessage(plugin.messages().text("&c" + result.message()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
player.sendMessage(plugin.messages().text("&aMining permit #" + result.id() + " bought for &f"
|
||||||
|
+ CoalMoney.format(result.cost()) + "&a. Expires " + Instant.ofEpochMilli(result.expiresAt()) + "."));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void list(Player player) throws SQLException {
|
||||||
|
List<Permit> permits = plugin.permitService().listActive(player.getUniqueId());
|
||||||
|
if (permits.isEmpty()) {
|
||||||
|
player.sendMessage(plugin.messages().text("&eYou have no active permits."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
player.sendMessage(plugin.messages().text("&eActive permits:"));
|
||||||
|
for (Permit permit : permits) {
|
||||||
|
String expiry = permit.expiresAt() == null ? "never" : Instant.ofEpochMilli(permit.expiresAt()).toString();
|
||||||
|
player.sendMessage(plugin.messages().raw("&7#" + permit.id() + " &f" + permit.permitType()
|
||||||
|
+ " " + (permit.regionName() == null ? "global" : permit.regionName()) + " expires " + expiry));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package com.librewiki.coalgov.command;
|
||||||
|
|
||||||
|
import com.librewiki.coalgov.CoalGovPlugin;
|
||||||
|
import com.librewiki.coalgov.util.CoalMoney;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.OfflinePlayer;
|
||||||
|
import org.bukkit.command.Command;
|
||||||
|
import org.bukkit.command.CommandExecutor;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public final class PoliceCommand implements CommandExecutor {
|
||||||
|
private final CoalGovPlugin plugin;
|
||||||
|
|
||||||
|
public PoliceCommand(CoalGovPlugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||||
|
if (!sender.hasPermission("coalgov.police")) {
|
||||||
|
sender.sendMessage(plugin.messages().text("&cYou lack permission."));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (args.length >= 1 && args[0].equalsIgnoreCase("fine")) {
|
||||||
|
fine(sender, args);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
usage(sender);
|
||||||
|
} catch (SQLException exception) {
|
||||||
|
plugin.getLogger().warning("Police command failed: " + exception.getMessage());
|
||||||
|
sender.sendMessage(plugin.messages().text("&cPolice records unavailable."));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fine(CommandSender sender, String[] args) throws SQLException {
|
||||||
|
if (args.length < 4) {
|
||||||
|
sender.sendMessage(plugin.messages().text("&eUsage: /police fine <player> <amount> <reason>"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
OfflinePlayer target = Bukkit.getOfflinePlayer(args[1]);
|
||||||
|
plugin.economyService().ensurePlayer(target);
|
||||||
|
long amount = CoalMoney.parsePositive(args[2]);
|
||||||
|
if (amount <= 0L) {
|
||||||
|
sender.sendMessage(plugin.messages().text("&cAmount must be greater than zero."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String reason = String.join(" ", Arrays.copyOfRange(args, 3, args.length));
|
||||||
|
UUID issuer = sender instanceof Player player ? player.getUniqueId() : null;
|
||||||
|
long id = plugin.governmentService().createFine(target.getUniqueId(), issuer, amount, reason);
|
||||||
|
sender.sendMessage(plugin.messages().text("&aFine #" + id + " issued to &f" + args[1] + " &afor &f" + CoalMoney.format(amount) + "&a."));
|
||||||
|
if (target.isOnline() && target.getPlayer() != null) {
|
||||||
|
target.getPlayer().sendMessage(plugin.messages().text("&cFine #" + id + ": &f" + CoalMoney.format(amount) + " &cfor " + reason + ". Use /coal fines pay " + id + "."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void usage(CommandSender sender) {
|
||||||
|
sender.sendMessage(plugin.messages().text("&eUsage: /police fine <player> <amount> <reason>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.librewiki.coalgov.listener;
|
||||||
|
|
||||||
|
import com.librewiki.coalgov.CoalGovPlugin;
|
||||||
|
import com.librewiki.coalgov.service.BuildProtectionService;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.block.BlockBreakEvent;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
public final class BlockBreakListener implements Listener {
|
||||||
|
private final CoalGovPlugin plugin;
|
||||||
|
|
||||||
|
public BlockBreakListener(CoalGovPlugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(ignoreCancelled = true)
|
||||||
|
public void onBlockBreak(BlockBreakEvent event) {
|
||||||
|
Material type = event.getBlock().getType();
|
||||||
|
try {
|
||||||
|
if (!plugin.hasAdminBypass(event.getPlayer()) && plugin.claimMarkerService().isMarkerBlock(event.getBlock())) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
event.getPlayer().sendMessage(plugin.messages().text("&cClaim corner markers are protected."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BuildProtectionService.BuildDecision decision = plugin.buildProtectionService()
|
||||||
|
.canModify(event.getPlayer(), event.getBlock().getLocation());
|
||||||
|
if (!decision.allowed()) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
event.getPlayer().sendMessage(plugin.messages().text("&c" + decision.message()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((type == Material.COAL_ORE || type == Material.DEEPSLATE_COAL_ORE)
|
||||||
|
&& !plugin.miningService().canMineCoal(event.getPlayer(), event.getBlock().getLocation())) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
event.getPlayer().sendMessage(plugin.messages().text("&cMining denied: this coal seam is regulated by the Ministry."));
|
||||||
|
}
|
||||||
|
} catch (SQLException exception) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
plugin.getLogger().warning("Block break check failed: " + exception.getMessage());
|
||||||
|
event.getPlayer().sendMessage(plugin.messages().text("&cLand registry unavailable. Try again later."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.librewiki.coalgov.listener;
|
||||||
|
|
||||||
|
import com.librewiki.coalgov.CoalGovPlugin;
|
||||||
|
import com.librewiki.coalgov.service.BuildProtectionService;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.block.BlockPlaceEvent;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
public final class BlockPlaceListener implements Listener {
|
||||||
|
private final CoalGovPlugin plugin;
|
||||||
|
|
||||||
|
public BlockPlaceListener(CoalGovPlugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(ignoreCancelled = true)
|
||||||
|
public void onBlockPlace(BlockPlaceEvent event) {
|
||||||
|
try {
|
||||||
|
if (!plugin.hasAdminBypass(event.getPlayer()) && plugin.claimMarkerService().isMarkerColumn(event.getBlockPlaced())) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
event.getPlayer().sendMessage(plugin.messages().text("&cClaim corner markers cannot be covered."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BuildProtectionService.BuildDecision decision = plugin.buildProtectionService()
|
||||||
|
.canModify(event.getPlayer(), event.getBlockPlaced().getLocation());
|
||||||
|
if (!decision.allowed()) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
event.getPlayer().sendMessage(plugin.messages().text("&c" + decision.message()));
|
||||||
|
}
|
||||||
|
} catch (SQLException exception) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
plugin.getLogger().warning("Build protection check failed: " + exception.getMessage());
|
||||||
|
event.getPlayer().sendMessage(plugin.messages().text("&cLand registry unavailable. Try again later."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.librewiki.coalgov.listener;
|
||||||
|
|
||||||
|
import com.librewiki.coalgov.CoalGovPlugin;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.block.Action;
|
||||||
|
import org.bukkit.event.player.PlayerInteractEvent;
|
||||||
|
|
||||||
|
public final class ClaimWandListener implements Listener {
|
||||||
|
private final CoalGovPlugin plugin;
|
||||||
|
|
||||||
|
public ClaimWandListener(CoalGovPlugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onPlayerInteract(PlayerInteractEvent event) {
|
||||||
|
if (!plugin.claimSelectionService().isWand(event.getItem())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.setCancelled(true);
|
||||||
|
if (event.getAction() != Action.RIGHT_CLICK_BLOCK || event.getClickedBlock() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.getPlayer().isSneaking()) {
|
||||||
|
plugin.claimSelectionService().clear(event.getPlayer());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
plugin.claimSelectionService().addPoint(event.getPlayer(), event.getClickedBlock().getLocation());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.librewiki.coalgov.listener;
|
||||||
|
|
||||||
|
import com.librewiki.coalgov.CoalGovPlugin;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.block.Action;
|
||||||
|
import org.bukkit.event.player.PlayerInteractEvent;
|
||||||
|
|
||||||
|
public final class DiviningRodListener implements Listener {
|
||||||
|
private final CoalGovPlugin plugin;
|
||||||
|
|
||||||
|
public DiviningRodListener(CoalGovPlugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onPlayerInteract(PlayerInteractEvent event) {
|
||||||
|
if (!plugin.diviningRodService().isRod(event.getItem())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.setCancelled(true);
|
||||||
|
if (event.getAction() != Action.RIGHT_CLICK_AIR && event.getAction() != Action.RIGHT_CLICK_BLOCK) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
plugin.diviningRodService().scan(event.getPlayer());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.librewiki.coalgov.listener;
|
||||||
|
|
||||||
|
import com.librewiki.coalgov.CoalGovPlugin;
|
||||||
|
import com.librewiki.coalgov.service.MarketService;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.inventory.ClickType;
|
||||||
|
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
public final class MarketListener implements Listener {
|
||||||
|
private final CoalGovPlugin plugin;
|
||||||
|
|
||||||
|
public MarketListener(CoalGovPlugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onInventoryClick(InventoryClickEvent event) {
|
||||||
|
if (!MarketService.TITLE.equals(event.getView().getTitle())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.setCancelled(true);
|
||||||
|
if (!(event.getWhoClicked() instanceof Player player)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (plugin.marketService().isSellAllSlot(event.getRawSlot())) {
|
||||||
|
plugin.marketService().sellAll(player);
|
||||||
|
player.closeInventory();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ItemStack clicked = event.getCurrentItem();
|
||||||
|
if (clicked == null || clicked.getType() == Material.AIR) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
plugin.marketService().buy(player, clicked.getType(), purchaseAmount(event.getClick()));
|
||||||
|
} catch (SQLException exception) {
|
||||||
|
plugin.getLogger().warning("Market click failed: " + exception.getMessage());
|
||||||
|
player.sendMessage(plugin.messages().text("&cMarket ledger unavailable."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int purchaseAmount(ClickType clickType) {
|
||||||
|
if (clickType.isShiftClick()) {
|
||||||
|
return 64;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package com.librewiki.coalgov.listener;
|
||||||
|
|
||||||
|
import com.librewiki.coalgov.CoalGovPlugin;
|
||||||
|
import com.librewiki.coalgov.model.CoalNpc;
|
||||||
|
import com.librewiki.coalgov.util.CoalMoney;
|
||||||
|
import net.citizensnpcs.api.event.NPCRightClickEvent;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public final class NpcListener implements Listener {
|
||||||
|
private final CoalGovPlugin plugin;
|
||||||
|
|
||||||
|
public NpcListener(CoalGovPlugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onNpcRightClick(NPCRightClickEvent event) {
|
||||||
|
try {
|
||||||
|
var coalNpc = plugin.npcService().findByCitizensId(event.getNPC().getId());
|
||||||
|
if (coalNpc.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.setCancelled(true);
|
||||||
|
showNpc(event.getClicker(), coalNpc.get());
|
||||||
|
} catch (SQLException exception) {
|
||||||
|
plugin.getLogger().warning("NPC click failed: " + exception.getMessage());
|
||||||
|
event.getClicker().sendMessage(plugin.messages().text("&cNPC registry unavailable."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showNpc(org.bukkit.entity.Player player, CoalNpc npc) throws SQLException {
|
||||||
|
player.sendMessage(plugin.messages().text("&e" + npc.name() + " &7(" + npc.npcType().toLowerCase() + " #" + npc.id() + ")"));
|
||||||
|
player.sendMessage(plugin.messages().text("&7Balance: &f" + CoalMoney.format(plugin.npcService().balance(npc))));
|
||||||
|
Map<Material, Long> inventory = plugin.npcService().inventory(npc.id());
|
||||||
|
if (inventory.isEmpty()) {
|
||||||
|
player.sendMessage(plugin.messages().text("&7Inventory: &fempty"));
|
||||||
|
} else {
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
int shown = 0;
|
||||||
|
for (var entry : inventory.entrySet()) {
|
||||||
|
if (shown++ > 0) {
|
||||||
|
builder.append(", ");
|
||||||
|
}
|
||||||
|
builder.append(entry.getKey().name().toLowerCase()).append(" ").append(entry.getValue());
|
||||||
|
if (shown >= 6) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
player.sendMessage(plugin.messages().text("&7Inventory: &f" + builder));
|
||||||
|
}
|
||||||
|
if (npc.trader()) {
|
||||||
|
player.sendMessage(plugin.messages().text("&7Haggle: &f/cgnpc haggle " + npc.id() + " buy <material> <amount> <offer>"));
|
||||||
|
player.sendMessage(plugin.messages().text("&7Or sell: &f/cgnpc haggle " + npc.id() + " sell <material> <amount> <offer>"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.librewiki.coalgov.listener;
|
||||||
|
|
||||||
|
import com.librewiki.coalgov.CoalGovPlugin;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.player.PlayerJoinEvent;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
public final class PlayerJoinListener implements Listener {
|
||||||
|
private final CoalGovPlugin plugin;
|
||||||
|
|
||||||
|
public PlayerJoinListener(CoalGovPlugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onPlayerJoin(PlayerJoinEvent event) {
|
||||||
|
try {
|
||||||
|
plugin.economyService().ensurePlayer(event.getPlayer());
|
||||||
|
} catch (SQLException exception) {
|
||||||
|
plugin.getLogger().warning("Failed to ensure player row: " + exception.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package com.librewiki.coalgov.listener;
|
||||||
|
|
||||||
|
import com.librewiki.coalgov.CoalGovPlugin;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.block.BlockBreakEvent;
|
||||||
|
import org.bukkit.event.block.BlockPlaceEvent;
|
||||||
|
import org.bukkit.event.inventory.FurnaceBurnEvent;
|
||||||
|
import org.bukkit.event.inventory.FurnaceStartSmeltEvent;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
public final class SuperFurnaceListener implements Listener {
|
||||||
|
private final CoalGovPlugin plugin;
|
||||||
|
|
||||||
|
public SuperFurnaceListener(CoalGovPlugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(ignoreCancelled = true)
|
||||||
|
public void onBlockPlace(BlockPlaceEvent event) {
|
||||||
|
if (event.getBlockPlaced().getType() != Material.FURNACE
|
||||||
|
|| !plugin.superFurnaceService().isItem(event.getItemInHand())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
plugin.superFurnaceService().add(event.getBlockPlaced());
|
||||||
|
event.getPlayer().sendMessage(plugin.messages().text("&aSuper furnace installed."));
|
||||||
|
} catch (SQLException exception) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
plugin.getLogger().warning("Super furnace placement failed: " + exception.getMessage());
|
||||||
|
event.getPlayer().sendMessage(plugin.messages().text("&cFurnace registry unavailable."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(ignoreCancelled = true)
|
||||||
|
public void onBlockBreak(BlockBreakEvent event) {
|
||||||
|
try {
|
||||||
|
if (!plugin.superFurnaceService().isSuperFurnace(event.getBlock())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
plugin.superFurnaceService().remove(event.getBlock());
|
||||||
|
event.setDropItems(false);
|
||||||
|
event.getBlock().getWorld().dropItemNaturally(event.getBlock().getLocation(), plugin.superFurnaceService().item());
|
||||||
|
} catch (SQLException exception) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
plugin.getLogger().warning("Super furnace break failed: " + exception.getMessage());
|
||||||
|
event.getPlayer().sendMessage(plugin.messages().text("&cFurnace registry unavailable."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(ignoreCancelled = true)
|
||||||
|
public void onFurnaceStartSmelt(FurnaceStartSmeltEvent event) {
|
||||||
|
try {
|
||||||
|
if (plugin.superFurnaceService().isSuperFurnace(event.getBlock())) {
|
||||||
|
event.setTotalCookTime(plugin.superFurnaceService().fasterCookTime(event.getTotalCookTime()));
|
||||||
|
}
|
||||||
|
} catch (SQLException exception) {
|
||||||
|
plugin.getLogger().warning("Super furnace smelt check failed: " + exception.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(ignoreCancelled = true)
|
||||||
|
public void onFurnaceBurn(FurnaceBurnEvent event) {
|
||||||
|
try {
|
||||||
|
if (plugin.superFurnaceService().isSuperFurnace(event.getBlock())) {
|
||||||
|
event.setBurnTime(plugin.superFurnaceService().shorterBurnTime(event.getBurnTime()));
|
||||||
|
}
|
||||||
|
} catch (SQLException exception) {
|
||||||
|
plugin.getLogger().warning("Super furnace burn check failed: " + exception.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
65
CoalGov/src/main/java/com/librewiki/coalgov/model/Claim.java
Normal file
65
CoalGov/src/main/java/com/librewiki/coalgov/model/Claim.java
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package com.librewiki.coalgov.model;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record Claim(
|
||||||
|
long id,
|
||||||
|
UUID ownerUuid,
|
||||||
|
String world,
|
||||||
|
int x1,
|
||||||
|
int z1,
|
||||||
|
int x2,
|
||||||
|
int z2,
|
||||||
|
String claimType,
|
||||||
|
Long expiresAt,
|
||||||
|
long taxDue,
|
||||||
|
long createdAt,
|
||||||
|
long purchaseCost,
|
||||||
|
List<ClaimPoint> vertices
|
||||||
|
) {
|
||||||
|
public Claim(
|
||||||
|
long id,
|
||||||
|
UUID ownerUuid,
|
||||||
|
String world,
|
||||||
|
int x1,
|
||||||
|
int z1,
|
||||||
|
int x2,
|
||||||
|
int z2,
|
||||||
|
String claimType,
|
||||||
|
Long expiresAt,
|
||||||
|
long taxDue,
|
||||||
|
long createdAt
|
||||||
|
) {
|
||||||
|
this(id, ownerUuid, world, x1, z1, x2, z2, claimType, expiresAt, taxDue, createdAt, -1L, List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean contains(String worldName, int x, int z) {
|
||||||
|
if (!world.equals(worldName) || x < x1 || x > x2 || z < z1 || z > z2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (vertices == null || vertices.size() < 3) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return containsPolygon(x + 0.5D, z + 0.5D);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPolygon() {
|
||||||
|
return vertices != null && vertices.size() >= 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean containsPolygon(double x, double z) {
|
||||||
|
boolean inside = false;
|
||||||
|
int size = vertices.size();
|
||||||
|
for (int i = 0, j = size - 1; i < size; j = i++) {
|
||||||
|
ClaimPoint a = vertices.get(i);
|
||||||
|
ClaimPoint b = vertices.get(j);
|
||||||
|
boolean intersects = ((a.z() > z) != (b.z() > z))
|
||||||
|
&& (x < (double) (b.x() - a.x()) * (z - a.z()) / (double) (b.z() - a.z()) + a.x());
|
||||||
|
if (intersects) {
|
||||||
|
inside = !inside;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return inside;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package com.librewiki.coalgov.model;
|
||||||
|
|
||||||
|
public record ClaimPoint(int x, int z) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.librewiki.coalgov.model;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public record CoalNpc(
|
||||||
|
long id,
|
||||||
|
int citizensId,
|
||||||
|
UUID accountUuid,
|
||||||
|
String npcType,
|
||||||
|
String name,
|
||||||
|
long zoneId,
|
||||||
|
String world,
|
||||||
|
double x,
|
||||||
|
double y,
|
||||||
|
double z
|
||||||
|
) {
|
||||||
|
public boolean trader() {
|
||||||
|
return npcType.equalsIgnoreCase("TRADER");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean worker() {
|
||||||
|
return npcType.equalsIgnoreCase("WORKER");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.librewiki.coalgov.model;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public record CoalTransaction(
|
||||||
|
long id,
|
||||||
|
UUID fromUuid,
|
||||||
|
UUID toUuid,
|
||||||
|
long amount,
|
||||||
|
String reason,
|
||||||
|
long createdAt
|
||||||
|
) {
|
||||||
|
}
|
||||||
17
CoalGov/src/main/java/com/librewiki/coalgov/model/Fine.java
Normal file
17
CoalGov/src/main/java/com/librewiki/coalgov/model/Fine.java
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package com.librewiki.coalgov.model;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public record Fine(
|
||||||
|
long id,
|
||||||
|
UUID playerUuid,
|
||||||
|
UUID issuerUuid,
|
||||||
|
long amount,
|
||||||
|
String reason,
|
||||||
|
Long paidAt,
|
||||||
|
long createdAt
|
||||||
|
) {
|
||||||
|
public boolean paid() {
|
||||||
|
return paidAt != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.librewiki.coalgov.model;
|
||||||
|
|
||||||
|
public enum LandClass {
|
||||||
|
FREEHOLD,
|
||||||
|
LEASE,
|
||||||
|
GOVERNMENT,
|
||||||
|
PUBLIC,
|
||||||
|
MINING_CONCESSION,
|
||||||
|
PROTECTED_PRESERVE,
|
||||||
|
BORDER_ZONE
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.librewiki.coalgov.model;
|
||||||
|
|
||||||
|
public record LandRegion(
|
||||||
|
long id,
|
||||||
|
String name,
|
||||||
|
String world,
|
||||||
|
int x1,
|
||||||
|
int z1,
|
||||||
|
int x2,
|
||||||
|
int z2,
|
||||||
|
LandClass landClass,
|
||||||
|
long createdAt
|
||||||
|
) {
|
||||||
|
public boolean contains(String worldName, int x, int z) {
|
||||||
|
return world.equals(worldName) && x >= x1 && x <= x2 && z >= z1 && z <= z2;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.librewiki.coalgov.model;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record NpcZone(
|
||||||
|
long id,
|
||||||
|
String name,
|
||||||
|
String world,
|
||||||
|
int x1,
|
||||||
|
int z1,
|
||||||
|
int x2,
|
||||||
|
int z2,
|
||||||
|
List<ClaimPoint> vertices
|
||||||
|
) {
|
||||||
|
public boolean contains(String worldName, int x, int z) {
|
||||||
|
if (!world.equals(worldName) || x < x1 || x > x2 || z < z1 || z > z2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (vertices == null || vertices.size() < 3) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
boolean inside = false;
|
||||||
|
int size = vertices.size();
|
||||||
|
double px = x + 0.5D;
|
||||||
|
double pz = z + 0.5D;
|
||||||
|
for (int i = 0, j = size - 1; i < size; j = i++) {
|
||||||
|
ClaimPoint a = vertices.get(i);
|
||||||
|
ClaimPoint b = vertices.get(j);
|
||||||
|
boolean intersects = ((a.z() > pz) != (b.z() > pz))
|
||||||
|
&& (px < (double) (b.x() - a.x()) * (pz - a.z()) / (double) (b.z() - a.z()) + a.x());
|
||||||
|
if (intersects) {
|
||||||
|
inside = !inside;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return inside;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.librewiki.coalgov.model;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public record Permit(
|
||||||
|
long id,
|
||||||
|
UUID ownerUuid,
|
||||||
|
PermitType permitType,
|
||||||
|
String regionName,
|
||||||
|
Long expiresAt,
|
||||||
|
long createdAt
|
||||||
|
) {
|
||||||
|
public boolean isActive(long now) {
|
||||||
|
return expiresAt == null || expiresAt > now;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.librewiki.coalgov.model;
|
||||||
|
|
||||||
|
public enum PermitType {
|
||||||
|
MINING,
|
||||||
|
BORDER_PASS,
|
||||||
|
GOVERNMENT_CONTRACTOR
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.librewiki.coalgov.service;
|
||||||
|
|
||||||
|
import com.librewiki.coalgov.CoalGovPlugin;
|
||||||
|
import com.librewiki.coalgov.model.Claim;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public final class BuildProtectionService {
|
||||||
|
private final CoalGovPlugin plugin;
|
||||||
|
private final ClaimService claimService;
|
||||||
|
|
||||||
|
public BuildProtectionService(CoalGovPlugin plugin, ClaimService claimService) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.claimService = claimService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BuildDecision canModify(Player player, Location location) throws SQLException {
|
||||||
|
if (plugin.hasAdminBypass(player)) {
|
||||||
|
return BuildDecision.allow();
|
||||||
|
}
|
||||||
|
Optional<Claim> claim = claimService.claimAt(location);
|
||||||
|
if (claim.isEmpty()) {
|
||||||
|
return BuildDecision.deny("This land is unclaimed and protected by the Ministry.");
|
||||||
|
}
|
||||||
|
if (!claim.get().ownerUuid().equals(player.getUniqueId())) {
|
||||||
|
return BuildDecision.deny("This claim belongs to another citizen.");
|
||||||
|
}
|
||||||
|
return BuildDecision.allow();
|
||||||
|
}
|
||||||
|
|
||||||
|
public record BuildDecision(boolean allowed, String message) {
|
||||||
|
public static BuildDecision allow() {
|
||||||
|
return new BuildDecision(true, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BuildDecision deny(String message) {
|
||||||
|
return new BuildDecision(false, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package com.librewiki.coalgov.service;
|
||||||
|
|
||||||
|
import com.librewiki.coalgov.CoalGovPlugin;
|
||||||
|
import com.librewiki.coalgov.model.Claim;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
public final class ClaimMarkerService {
|
||||||
|
private static final Material MARKER_MATERIAL = Material.GOLD_BLOCK;
|
||||||
|
|
||||||
|
private final CoalGovPlugin plugin;
|
||||||
|
private final ClaimService claimService;
|
||||||
|
|
||||||
|
public ClaimMarkerService(CoalGovPlugin plugin, ClaimService claimService) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.claimService = claimService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void placeMarkers(Claim claim) {
|
||||||
|
World world = plugin.getServer().getWorld(claim.world());
|
||||||
|
if (world == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (claim.isPolygon()) {
|
||||||
|
for (var point : claim.vertices()) {
|
||||||
|
placeMarker(world, point.x(), point.z());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
placeMarker(world, claim.x1(), claim.z1());
|
||||||
|
placeMarker(world, claim.x1(), claim.z2());
|
||||||
|
placeMarker(world, claim.x2(), claim.z1());
|
||||||
|
placeMarker(world, claim.x2(), claim.z2());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeMarkers(Claim claim) {
|
||||||
|
World world = plugin.getServer().getWorld(claim.world());
|
||||||
|
if (world == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (claim.isPolygon()) {
|
||||||
|
for (var point : claim.vertices()) {
|
||||||
|
removeMarkerColumn(world, point.x(), point.z());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
removeMarkerColumn(world, claim.x1(), claim.z1());
|
||||||
|
removeMarkerColumn(world, claim.x1(), claim.z2());
|
||||||
|
removeMarkerColumn(world, claim.x2(), claim.z1());
|
||||||
|
removeMarkerColumn(world, claim.x2(), claim.z2());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void placeAllLoadedMarkers() {
|
||||||
|
try {
|
||||||
|
for (Claim claim : claimService.listAll()) {
|
||||||
|
placeMarkers(claim);
|
||||||
|
}
|
||||||
|
} catch (SQLException exception) {
|
||||||
|
plugin.getLogger().warning("Failed to place claim markers: " + exception.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isMarkerBlock(Block block) throws SQLException {
|
||||||
|
if (block.getType() != MARKER_MATERIAL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return isMarkerColumn(block);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isMarkerColumn(Block block) throws SQLException {
|
||||||
|
return claimService.claimAt(block.getLocation())
|
||||||
|
.map(claim -> isCorner(claim, block.getX(), block.getZ()))
|
||||||
|
.orElse(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void placeMarker(World world, int x, int z) {
|
||||||
|
int y = world.getHighestBlockYAt(x, z);
|
||||||
|
Block highest = world.getBlockAt(x, y, z);
|
||||||
|
if (highest.getType() == MARKER_MATERIAL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
world.getBlockAt(x, y + 1, z).setType(MARKER_MATERIAL, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeMarkerColumn(World world, int x, int z) {
|
||||||
|
for (int y = world.getMinHeight(); y < world.getMaxHeight(); y++) {
|
||||||
|
Block block = world.getBlockAt(x, y, z);
|
||||||
|
if (block.getType() == MARKER_MATERIAL) {
|
||||||
|
block.setType(Material.AIR, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isCorner(Claim claim, int x, int z) {
|
||||||
|
if (claim.isPolygon()) {
|
||||||
|
return claim.vertices().stream().anyMatch(point -> point.x() == x && point.z() == z);
|
||||||
|
}
|
||||||
|
return (x == claim.x1() || x == claim.x2()) && (z == claim.z1() || z == claim.z2());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
package com.librewiki.coalgov.service;
|
||||||
|
|
||||||
|
import com.librewiki.coalgov.CoalGovPlugin;
|
||||||
|
import com.librewiki.coalgov.model.ClaimPoint;
|
||||||
|
import org.bukkit.Color;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.NamespacedKey;
|
||||||
|
import org.bukkit.Particle;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import org.bukkit.inventory.meta.ItemMeta;
|
||||||
|
import org.bukkit.persistence.PersistentDataType;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public final class ClaimSelectionService {
|
||||||
|
private final CoalGovPlugin plugin;
|
||||||
|
private final NamespacedKey wandKey;
|
||||||
|
private final Map<UUID, List<Location>> selections = new HashMap<>();
|
||||||
|
private final Map<UUID, List<Location>> markerPreviews = new HashMap<>();
|
||||||
|
|
||||||
|
public ClaimSelectionService(CoalGovPlugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.wandKey = new NamespacedKey(plugin, "claim_wand");
|
||||||
|
}
|
||||||
|
|
||||||
|
public ItemStack wand() {
|
||||||
|
ItemStack item = new ItemStack(Material.BLAZE_ROD);
|
||||||
|
ItemMeta meta = item.getItemMeta();
|
||||||
|
meta.setDisplayName(plugin.messages().raw("&6CoalGov Claim Wand"));
|
||||||
|
meta.setLore(List.of(
|
||||||
|
plugin.messages().raw("&7Right-click blocks to add polygon points."),
|
||||||
|
plugin.messages().raw("&7Sneak right-click to clear.")
|
||||||
|
));
|
||||||
|
meta.getPersistentDataContainer().set(wandKey, PersistentDataType.BYTE, (byte) 1);
|
||||||
|
item.setItemMeta(meta);
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isWand(ItemStack item) {
|
||||||
|
if (item == null || item.getType() != Material.BLAZE_ROD || !item.hasItemMeta()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return item.getItemMeta().getPersistentDataContainer().has(wandKey, PersistentDataType.BYTE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addPoint(Player player, Location clicked) {
|
||||||
|
List<Location> points = selections.computeIfAbsent(player.getUniqueId(), ignored -> new ArrayList<>());
|
||||||
|
if (!points.isEmpty() && !points.get(0).getWorld().equals(clicked.getWorld())) {
|
||||||
|
restoreMarkerPreviews(player);
|
||||||
|
points.clear();
|
||||||
|
}
|
||||||
|
points.add(clicked.toBlockLocation());
|
||||||
|
player.sendMessage(plugin.messages().text("&aPolygon point " + points.size() + " set at "
|
||||||
|
+ clicked.getBlockX() + ", " + clicked.getBlockZ() + "."));
|
||||||
|
show(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear(Player player) {
|
||||||
|
restoreMarkerPreviews(player);
|
||||||
|
selections.remove(player.getUniqueId());
|
||||||
|
player.sendMessage(plugin.messages().text("&aClaim polygon cleared."));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Location> points(Player player) {
|
||||||
|
return selections.getOrDefault(player.getUniqueId(), List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ClaimPoint> claimPoints(Player player) {
|
||||||
|
List<ClaimPoint> points = new ArrayList<>();
|
||||||
|
for (Location location : points(player)) {
|
||||||
|
points.add(new ClaimPoint(location.getBlockX(), location.getBlockZ()));
|
||||||
|
}
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void consume(Player player) {
|
||||||
|
restoreMarkerPreviews(player);
|
||||||
|
selections.remove(player.getUniqueId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void show(Player player) {
|
||||||
|
List<Location> points = points(player);
|
||||||
|
if (points.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showMarkerPreviews(player, points);
|
||||||
|
Particle.DustOptions dust = new Particle.DustOptions(Color.fromRGB(64, 210, 255), 1.4f);
|
||||||
|
Particle.DustOptions vertexDust = new Particle.DustOptions(Color.fromRGB(255, 220, 80), 1.8f);
|
||||||
|
for (Location point : points) {
|
||||||
|
spawn(player, point.getWorld(), point.getBlockX(), point.getBlockZ(), vertexDust, 8);
|
||||||
|
}
|
||||||
|
for (int i = 1; i < points.size(); i++) {
|
||||||
|
drawLine(player, points.get(i - 1), points.get(i), dust);
|
||||||
|
}
|
||||||
|
if (points.size() >= 3) {
|
||||||
|
drawLine(player, points.get(points.size() - 1), points.get(0), dust);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawLine(Player player, Location a, Location b, Particle.DustOptions dust) {
|
||||||
|
if (!a.getWorld().equals(b.getWorld())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
double dx = b.getBlockX() - a.getBlockX();
|
||||||
|
double dz = b.getBlockZ() - a.getBlockZ();
|
||||||
|
int steps = Math.max(1, (int) Math.ceil(Math.max(Math.abs(dx), Math.abs(dz)) / 2.0D));
|
||||||
|
for (int i = 0; i <= steps; i++) {
|
||||||
|
double t = (double) i / (double) steps;
|
||||||
|
int x = (int) Math.round(a.getBlockX() + dx * t);
|
||||||
|
int z = (int) Math.round(a.getBlockZ() + dz * t);
|
||||||
|
spawn(player, a.getWorld(), x, z, dust, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void spawn(Player player, World world, int x, int z, Particle.DustOptions dust, int count) {
|
||||||
|
double y = world.getHighestBlockYAt(x, z) + 1.2D;
|
||||||
|
player.spawnParticle(Particle.DUST, x + 0.5D, y, z + 0.5D, count, 0.15D, 0.2D, 0.15D, 0.0D, dust);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showMarkerPreviews(Player player, List<Location> points) {
|
||||||
|
restoreMarkerPreviews(player);
|
||||||
|
List<Location> previews = new ArrayList<>();
|
||||||
|
for (Location point : points) {
|
||||||
|
World world = point.getWorld();
|
||||||
|
if (world == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Location marker = markerLocation(world, point.getBlockX(), point.getBlockZ());
|
||||||
|
player.sendBlockChange(marker, Material.GOLD_BLOCK.createBlockData());
|
||||||
|
previews.add(marker);
|
||||||
|
}
|
||||||
|
markerPreviews.put(player.getUniqueId(), previews);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void restoreMarkerPreviews(Player player) {
|
||||||
|
List<Location> previews = markerPreviews.remove(player.getUniqueId());
|
||||||
|
if (previews == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (Location preview : previews) {
|
||||||
|
World world = preview.getWorld();
|
||||||
|
if (world == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Block block = world.getBlockAt(preview);
|
||||||
|
player.sendBlockChange(preview, block.getBlockData());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Location markerLocation(World world, int x, int z) {
|
||||||
|
int y = world.getHighestBlockYAt(x, z);
|
||||||
|
Block highest = world.getBlockAt(x, y, z);
|
||||||
|
if (highest.getType() == Material.GOLD_BLOCK) {
|
||||||
|
return highest.getLocation();
|
||||||
|
}
|
||||||
|
return world.getBlockAt(x, y + 1, z).getLocation();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
package com.librewiki.coalgov.service;
|
||||||
|
|
||||||
|
import com.librewiki.coalgov.model.Claim;
|
||||||
|
import com.librewiki.coalgov.model.ClaimPoint;
|
||||||
|
import com.librewiki.coalgov.storage.ClaimRepository;
|
||||||
|
import com.librewiki.coalgov.util.CoalMoney;
|
||||||
|
import com.librewiki.coalgov.util.Cuboid2D;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.configuration.file.FileConfiguration;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public final class ClaimService {
|
||||||
|
private final ClaimRepository repository;
|
||||||
|
private final LandService landService;
|
||||||
|
private final EconomyService economyService;
|
||||||
|
private FileConfiguration config;
|
||||||
|
|
||||||
|
public ClaimService(ClaimRepository repository, LandService landService, EconomyService economyService, FileConfiguration config) {
|
||||||
|
this.repository = repository;
|
||||||
|
this.landService = landService;
|
||||||
|
this.economyService = economyService;
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConfig(FileConfiguration config) {
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BuyResult buy(UUID owner, Location center, String claimType, int radius) throws SQLException {
|
||||||
|
String type = claimType.toLowerCase();
|
||||||
|
if (!type.equals("homestead") && !type.equals("industrial")) {
|
||||||
|
return BuyResult.fail("Claim type must be homestead or industrial.");
|
||||||
|
}
|
||||||
|
int maxRadius = config.getInt("claims." + type + ".max_radius");
|
||||||
|
if (radius <= 0 || radius > maxRadius) {
|
||||||
|
return BuyResult.fail("Radius must be between 1 and " + maxRadius + ".");
|
||||||
|
}
|
||||||
|
Cuboid2D area = new Cuboid2D(
|
||||||
|
center.getWorld().getName(),
|
||||||
|
center.getBlockX() - radius,
|
||||||
|
center.getBlockZ() - radius,
|
||||||
|
center.getBlockX() + radius,
|
||||||
|
center.getBlockZ() + radius
|
||||||
|
);
|
||||||
|
if (repository.overlaps(area)) {
|
||||||
|
return BuyResult.fail("That claim overlaps an existing claim.");
|
||||||
|
}
|
||||||
|
if (landService.blocksPrivateClaim(area)) {
|
||||||
|
return BuyResult.fail("That land class cannot be claimed.");
|
||||||
|
}
|
||||||
|
long cost = adjustedCost(owner, type, area.area(), area);
|
||||||
|
if (cost > 0L && !economyService.charge(owner, cost, "claim_purchase")) {
|
||||||
|
return BuyResult.fail("You need " + CoalMoney.format(cost) + " for that claim.");
|
||||||
|
}
|
||||||
|
long id = repository.create(owner, area, type.toUpperCase(), null, cost, List.of());
|
||||||
|
return BuyResult.success(id, cost);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BuyResult buyPolygon(UUID owner, String world, String claimType, List<ClaimPoint> vertices) throws SQLException {
|
||||||
|
String type = claimType.toLowerCase();
|
||||||
|
if (!type.equals("homestead") && !type.equals("industrial")) {
|
||||||
|
return BuyResult.fail("Claim type must be homestead or industrial.");
|
||||||
|
}
|
||||||
|
if (vertices.size() < 3) {
|
||||||
|
return BuyResult.fail("Use the claim wand to set at least 3 polygon points.");
|
||||||
|
}
|
||||||
|
Cuboid2D area = bounds(world, vertices);
|
||||||
|
int maxRadius = config.getInt("claims." + type + ".max_radius");
|
||||||
|
int maxWidth = maxRadius * 2 + 1;
|
||||||
|
if ((area.x2() - area.x1() + 1) > maxWidth || (area.z2() - area.z1() + 1) > maxWidth) {
|
||||||
|
return BuyResult.fail("Polygon bounds exceed max " + type + " size.");
|
||||||
|
}
|
||||||
|
if (repository.overlaps(area)) {
|
||||||
|
return BuyResult.fail("That claim overlaps an existing claim.");
|
||||||
|
}
|
||||||
|
if (landService.blocksPrivateClaim(area)) {
|
||||||
|
return BuyResult.fail("That land class cannot be claimed.");
|
||||||
|
}
|
||||||
|
long cost = adjustedCost(owner, type, polygonArea(vertices), area);
|
||||||
|
if (cost > 0L && !economyService.charge(owner, cost, "claim_purchase")) {
|
||||||
|
return BuyResult.fail("You need " + CoalMoney.format(cost) + " for that claim.");
|
||||||
|
}
|
||||||
|
long id = repository.create(owner, area, type.toUpperCase(), null, cost, vertices);
|
||||||
|
return BuyResult.success(id, cost);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Claim> claimAt(Location location) throws SQLException {
|
||||||
|
return repository.findAt(location.getWorld().getName(), location.getBlockX(), location.getBlockZ());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Claim> list(UUID owner) throws SQLException {
|
||||||
|
return repository.findByOwner(owner);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Claim> listAll() throws SQLException {
|
||||||
|
return repository.listAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Claim> findOwnedById(UUID owner, long id) throws SQLException {
|
||||||
|
return repository.findOwnedById(owner, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SellResult sell(UUID owner, long id, long refund) throws SQLException {
|
||||||
|
Optional<Claim> claim = repository.findOwnedById(owner, id);
|
||||||
|
if (claim.isEmpty()) {
|
||||||
|
return SellResult.fail("No owned claim with that id.");
|
||||||
|
}
|
||||||
|
if (refund < 0L && !economyService.charge(owner, Math.abs(refund), "claim_sale_depletion")) {
|
||||||
|
return SellResult.fail("You need " + CoalMoney.format(Math.abs(refund)) + " to settle extracted land value.");
|
||||||
|
}
|
||||||
|
if (!repository.deleteOwned(owner, id)) {
|
||||||
|
if (refund < 0L) {
|
||||||
|
economyService.credit(owner, Math.abs(refund), "claim_sale_depletion_refund");
|
||||||
|
}
|
||||||
|
return SellResult.fail("Could not sell that claim.");
|
||||||
|
}
|
||||||
|
if (refund > 0L) {
|
||||||
|
economyService.credit(owner, refund, "claim_sale");
|
||||||
|
}
|
||||||
|
return SellResult.success(claim.get(), refund);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean abandon(UUID owner, long id) throws SQLException {
|
||||||
|
return repository.deleteOwned(owner, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TransferResult transfer(UUID owner, long id, UUID newOwner) throws SQLException {
|
||||||
|
if (owner.equals(newOwner)) {
|
||||||
|
return TransferResult.fail("You already own that claim.");
|
||||||
|
}
|
||||||
|
Optional<Claim> claim = repository.findOwnedById(owner, id);
|
||||||
|
if (claim.isEmpty()) {
|
||||||
|
return TransferResult.fail("No owned claim with that id.");
|
||||||
|
}
|
||||||
|
if (!repository.transferOwned(owner, id, newOwner)) {
|
||||||
|
return TransferResult.fail("Could not transfer that claim.");
|
||||||
|
}
|
||||||
|
return TransferResult.success(claim.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
public long claimPurchaseCost(Claim claim) {
|
||||||
|
if (claim.purchaseCost() >= 0L) {
|
||||||
|
return claim.purchaseCost();
|
||||||
|
}
|
||||||
|
return assessedClaimCost(claim);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long assessedClaimCost(Claim claim) {
|
||||||
|
return cost(claim.claimType().toLowerCase(), (long) (claim.x2() - claim.x1() + 1) * (claim.z2() - claim.z1() + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
private long cost(String type, long area) {
|
||||||
|
double base = config.getDouble("claims." + type + ".base_cost");
|
||||||
|
double perBlock = config.getDouble("claims." + type + ".cost_per_block");
|
||||||
|
return CoalMoney.fromCoalConfig(base + area * perBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long adjustedCost(UUID owner, String type, long areaBlocks, Cuboid2D bounds) throws SQLException {
|
||||||
|
long cost = cost(type, areaBlocks);
|
||||||
|
if (!type.equals("homestead") || !config.getBoolean("claims.homestead.first_claim.free", true)) {
|
||||||
|
return cost;
|
||||||
|
}
|
||||||
|
if (!list(owner).isEmpty()) {
|
||||||
|
return cost;
|
||||||
|
}
|
||||||
|
int freeRadius = config.getInt("claims.homestead.first_claim.max_radius", 12);
|
||||||
|
int maxWidth = freeRadius * 2 + 1;
|
||||||
|
if ((bounds.x2() - bounds.x1() + 1) > maxWidth || (bounds.z2() - bounds.z1() + 1) > maxWidth) {
|
||||||
|
return cost;
|
||||||
|
}
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Cuboid2D bounds(String world, List<ClaimPoint> vertices) {
|
||||||
|
int minX = vertices.stream().mapToInt(ClaimPoint::x).min().orElse(0);
|
||||||
|
int maxX = vertices.stream().mapToInt(ClaimPoint::x).max().orElse(0);
|
||||||
|
int minZ = vertices.stream().mapToInt(ClaimPoint::z).min().orElse(0);
|
||||||
|
int maxZ = vertices.stream().mapToInt(ClaimPoint::z).max().orElse(0);
|
||||||
|
return new Cuboid2D(world, minX, minZ, maxX, maxZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long polygonArea(List<ClaimPoint> vertices) {
|
||||||
|
long sum = 0L;
|
||||||
|
for (int i = 0, j = vertices.size() - 1; i < vertices.size(); j = i++) {
|
||||||
|
ClaimPoint a = vertices.get(j);
|
||||||
|
ClaimPoint b = vertices.get(i);
|
||||||
|
sum += (long) a.x() * b.z() - (long) b.x() * a.z();
|
||||||
|
}
|
||||||
|
return Math.max(1L, Math.round(Math.abs(sum) / 2.0D));
|
||||||
|
}
|
||||||
|
|
||||||
|
public record BuyResult(boolean success, String message, long id, long cost) {
|
||||||
|
public static BuyResult fail(String message) {
|
||||||
|
return new BuyResult(false, message, -1L, 0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BuyResult success(long id, long cost) {
|
||||||
|
return new BuyResult(true, "", id, cost);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record SellResult(boolean success, String message, Claim claim, long refund) {
|
||||||
|
public static SellResult fail(String message) {
|
||||||
|
return new SellResult(false, message, null, 0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SellResult success(Claim claim, long refund) {
|
||||||
|
return new SellResult(true, "", claim, refund);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record TransferResult(boolean success, String message, Claim claim) {
|
||||||
|
public static TransferResult fail(String message) {
|
||||||
|
return new TransferResult(false, message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TransferResult success(Claim claim) {
|
||||||
|
return new TransferResult(true, "", claim);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package com.librewiki.coalgov.service;
|
||||||
|
|
||||||
|
import com.librewiki.coalgov.CoalGovPlugin;
|
||||||
|
import com.librewiki.coalgov.model.Claim;
|
||||||
|
import org.bukkit.Color;
|
||||||
|
import org.bukkit.Particle;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.scheduler.BukkitRunnable;
|
||||||
|
|
||||||
|
public final class ClaimVisualService {
|
||||||
|
private static final int REPEATS = 12;
|
||||||
|
private static final int PERIOD_TICKS = 10;
|
||||||
|
private static final int STEP_BLOCKS = 2;
|
||||||
|
|
||||||
|
private final CoalGovPlugin plugin;
|
||||||
|
|
||||||
|
public ClaimVisualService(CoalGovPlugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void show(Player player, Claim claim) {
|
||||||
|
World world = plugin.getServer().getWorld(claim.world());
|
||||||
|
if (world == null) {
|
||||||
|
player.sendMessage(plugin.messages().text("&cThat claim's world is not loaded."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!player.getWorld().equals(world)) {
|
||||||
|
player.sendMessage(plugin.messages().text("&cThat claim is in world " + claim.world() + "."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Particle.DustOptions edgeDust = new Particle.DustOptions(Color.fromRGB(255, 196, 64), 1.4f);
|
||||||
|
Particle.DustOptions cornerDust = new Particle.DustOptions(Color.fromRGB(255, 80, 64), 1.8f);
|
||||||
|
new BukkitRunnable() {
|
||||||
|
private int remaining = REPEATS;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (remaining-- <= 0 || !player.isOnline()) {
|
||||||
|
cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
draw(player, world, claim, edgeDust, cornerDust);
|
||||||
|
}
|
||||||
|
}.runTaskTimer(plugin, 0L, PERIOD_TICKS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void draw(Player player, World world, Claim claim, Particle.DustOptions edgeDust, Particle.DustOptions cornerDust) {
|
||||||
|
if (claim.isPolygon()) {
|
||||||
|
for (int i = 0; i < claim.vertices().size(); i++) {
|
||||||
|
var a = claim.vertices().get(i);
|
||||||
|
var b = claim.vertices().get((i + 1) % claim.vertices().size());
|
||||||
|
drawLine(player, world, a.x(), a.z(), b.x(), b.z(), edgeDust);
|
||||||
|
spawnCorner(player, world, a.x(), a.z(), cornerDust);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int x = claim.x1(); x <= claim.x2(); x += STEP_BLOCKS) {
|
||||||
|
spawnEdge(player, world, x, claim.z1(), edgeDust);
|
||||||
|
spawnEdge(player, world, x, claim.z2(), edgeDust);
|
||||||
|
}
|
||||||
|
for (int z = claim.z1(); z <= claim.z2(); z += STEP_BLOCKS) {
|
||||||
|
spawnEdge(player, world, claim.x1(), z, edgeDust);
|
||||||
|
spawnEdge(player, world, claim.x2(), z, edgeDust);
|
||||||
|
}
|
||||||
|
spawnCorner(player, world, claim.x1(), claim.z1(), cornerDust);
|
||||||
|
spawnCorner(player, world, claim.x1(), claim.z2(), cornerDust);
|
||||||
|
spawnCorner(player, world, claim.x2(), claim.z1(), cornerDust);
|
||||||
|
spawnCorner(player, world, claim.x2(), claim.z2(), cornerDust);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void spawnEdge(Player player, World world, int x, int z, Particle.DustOptions dust) {
|
||||||
|
double y = world.getHighestBlockYAt(x, z) + 1.15;
|
||||||
|
player.spawnParticle(Particle.DUST, x + 0.5, y, z + 0.5, 1, 0.0, 0.15, 0.0, 0.0, dust);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawLine(Player player, World world, int x1, int z1, int x2, int z2, Particle.DustOptions dust) {
|
||||||
|
double dx = x2 - x1;
|
||||||
|
double dz = z2 - z1;
|
||||||
|
int steps = Math.max(1, (int) Math.ceil(Math.max(Math.abs(dx), Math.abs(dz)) / STEP_BLOCKS));
|
||||||
|
for (int i = 0; i <= steps; i++) {
|
||||||
|
double t = (double) i / (double) steps;
|
||||||
|
spawnEdge(player, world, (int) Math.round(x1 + dx * t), (int) Math.round(z1 + dz * t), dust);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void spawnCorner(Player player, World world, int x, int z, Particle.DustOptions dust) {
|
||||||
|
double y = world.getHighestBlockYAt(x, z) + 1.25;
|
||||||
|
player.spawnParticle(Particle.DUST, x + 0.5, y, z + 0.5, 8, 0.25, 0.35, 0.25, 0.0, dust);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
package com.librewiki.coalgov.service;
|
||||||
|
|
||||||
|
import com.librewiki.coalgov.CoalGovPlugin;
|
||||||
|
import org.bukkit.Color;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.NamespacedKey;
|
||||||
|
import org.bukkit.Particle;
|
||||||
|
import org.bukkit.Sound;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import org.bukkit.inventory.meta.ItemMeta;
|
||||||
|
import org.bukkit.persistence.PersistentDataType;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public final class DiviningRodService {
|
||||||
|
private static final List<Material> ORES = List.of(
|
||||||
|
Material.COAL_ORE,
|
||||||
|
Material.DEEPSLATE_COAL_ORE,
|
||||||
|
Material.COPPER_ORE,
|
||||||
|
Material.DEEPSLATE_COPPER_ORE,
|
||||||
|
Material.IRON_ORE,
|
||||||
|
Material.DEEPSLATE_IRON_ORE,
|
||||||
|
Material.GOLD_ORE,
|
||||||
|
Material.DEEPSLATE_GOLD_ORE,
|
||||||
|
Material.REDSTONE_ORE,
|
||||||
|
Material.DEEPSLATE_REDSTONE_ORE,
|
||||||
|
Material.LAPIS_ORE,
|
||||||
|
Material.DEEPSLATE_LAPIS_ORE,
|
||||||
|
Material.DIAMOND_ORE,
|
||||||
|
Material.DEEPSLATE_DIAMOND_ORE,
|
||||||
|
Material.EMERALD_ORE,
|
||||||
|
Material.DEEPSLATE_EMERALD_ORE,
|
||||||
|
Material.NETHER_GOLD_ORE,
|
||||||
|
Material.NETHER_QUARTZ_ORE,
|
||||||
|
Material.ANCIENT_DEBRIS
|
||||||
|
);
|
||||||
|
|
||||||
|
private final CoalGovPlugin plugin;
|
||||||
|
private final NamespacedKey rodKey;
|
||||||
|
private final Map<UUID, Long> lastUseMillis = new HashMap<>();
|
||||||
|
|
||||||
|
public DiviningRodService(CoalGovPlugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.rodKey = new NamespacedKey(plugin, "divining_rod");
|
||||||
|
}
|
||||||
|
|
||||||
|
public ItemStack rod() {
|
||||||
|
ItemStack item = new ItemStack(Material.BLAZE_ROD);
|
||||||
|
ItemMeta meta = item.getItemMeta();
|
||||||
|
meta.setDisplayName(plugin.messages().raw("&dCoalGov Divining Rod"));
|
||||||
|
meta.setLore(List.of(
|
||||||
|
plugin.messages().raw("&7Right-click to test the ground for ore."),
|
||||||
|
plugin.messages().raw("&7Issued by the Ministry Geological Survey.")
|
||||||
|
));
|
||||||
|
meta.getPersistentDataContainer().set(rodKey, PersistentDataType.BYTE, (byte) 1);
|
||||||
|
item.setItemMeta(meta);
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRod(ItemStack item) {
|
||||||
|
if (item == null || item.getType() != Material.BLAZE_ROD || !item.hasItemMeta()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return item.getItemMeta().getPersistentDataContainer().has(rodKey, PersistentDataType.BYTE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void scan(Player player) {
|
||||||
|
long remaining = cooldownRemainingMillis(player);
|
||||||
|
if (remaining > 0L) {
|
||||||
|
player.sendMessage(plugin.messages().text("&cThe rod is still settling. Try again in "
|
||||||
|
+ Math.ceil(remaining / 1000.0D) + "s."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastUseMillis.put(player.getUniqueId(), System.currentTimeMillis());
|
||||||
|
|
||||||
|
ScanHit hit = nearestOre(player.getLocation());
|
||||||
|
if (hit == null) {
|
||||||
|
player.playSound(player.getLocation(), Sound.BLOCK_AMETHYST_BLOCK_CHIME, 0.35f, 0.55f);
|
||||||
|
player.spawnParticle(Particle.ASH, player.getLocation().add(0.0D, 1.0D, 0.0D), 10, 0.35D, 0.2D, 0.35D, 0.0D);
|
||||||
|
player.sendMessage(plugin.messages().text("&7The divining rod stays still. No ore signal nearby."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
double strength = 1.0D - Math.min(1.0D, hit.distance() / Math.max(1.0D, scanDepth()));
|
||||||
|
float pitch = (float) (0.75D + strength);
|
||||||
|
int particles = 8 + (int) Math.round(strength * 18.0D);
|
||||||
|
player.playSound(player.getLocation(), Sound.BLOCK_AMETHYST_BLOCK_RESONATE, 0.75f, pitch);
|
||||||
|
player.spawnParticle(Particle.DUST, player.getLocation().add(0.0D, 1.0D, 0.0D), particles,
|
||||||
|
0.4D, 0.25D, 0.4D, 0.0D, new Particle.DustOptions(Color.fromRGB(190, 80, 255), 1.1f));
|
||||||
|
player.sendMessage(plugin.messages().text("&dThe divining rod pulls " + direction(hit)
|
||||||
|
+ " toward &f" + readable(hit.material()) + " &7(" + Math.round(hit.distance()) + " blocks, "
|
||||||
|
+ signalName(strength) + " signal)&d."));
|
||||||
|
}
|
||||||
|
|
||||||
|
private long cooldownRemainingMillis(Player player) {
|
||||||
|
long cooldownMillis = Math.max(0L, Math.round(plugin.getConfig()
|
||||||
|
.getDouble("divining_rod.cooldown_seconds", 5.0D) * 1000.0D));
|
||||||
|
long lastUse = lastUseMillis.getOrDefault(player.getUniqueId(), 0L);
|
||||||
|
return Math.max(0L, cooldownMillis - (System.currentTimeMillis() - lastUse));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ScanHit nearestOre(Location origin) {
|
||||||
|
World world = origin.getWorld();
|
||||||
|
if (world == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int radius = scanRadius();
|
||||||
|
int depth = scanDepth();
|
||||||
|
int originX = origin.getBlockX();
|
||||||
|
int originY = origin.getBlockY();
|
||||||
|
int originZ = origin.getBlockZ();
|
||||||
|
int minY = Math.max(world.getMinHeight(), originY - depth);
|
||||||
|
ScanHit nearest = null;
|
||||||
|
for (int x = originX - radius; x <= originX + radius; x++) {
|
||||||
|
for (int z = originZ - radius; z <= originZ + radius; z++) {
|
||||||
|
for (int y = originY; y >= minY; y--) {
|
||||||
|
Material material = world.getBlockAt(x, y, z).getType();
|
||||||
|
if (!ORES.contains(material)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
double distance = origin.distance(new Location(world, x + 0.5D, y + 0.5D, z + 0.5D));
|
||||||
|
if (nearest == null || distance < nearest.distance()) {
|
||||||
|
nearest = new ScanHit(material, x - originX, y - originY, z - originZ, distance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nearest;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int scanRadius() {
|
||||||
|
return Math.max(0, plugin.getConfig().getInt("divining_rod.scan_radius", 4));
|
||||||
|
}
|
||||||
|
|
||||||
|
private int scanDepth() {
|
||||||
|
return Math.max(1, plugin.getConfig().getInt("divining_rod.scan_depth", 48));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String direction(ScanHit hit) {
|
||||||
|
if (hit.dx() == 0 && hit.dz() == 0) {
|
||||||
|
return "straight down";
|
||||||
|
}
|
||||||
|
String northSouth = hit.dz() < 0 ? "north" : hit.dz() > 0 ? "south" : "";
|
||||||
|
String eastWest = hit.dx() < 0 ? "west" : hit.dx() > 0 ? "east" : "";
|
||||||
|
if (northSouth.isEmpty()) {
|
||||||
|
return eastWest + " and down";
|
||||||
|
}
|
||||||
|
if (eastWest.isEmpty()) {
|
||||||
|
return northSouth + " and down";
|
||||||
|
}
|
||||||
|
return northSouth + "-" + eastWest + " and down";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String signalName(double strength) {
|
||||||
|
if (strength >= 0.75D) {
|
||||||
|
return "strong";
|
||||||
|
}
|
||||||
|
if (strength >= 0.45D) {
|
||||||
|
return "steady";
|
||||||
|
}
|
||||||
|
return "faint";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String readable(Material material) {
|
||||||
|
return material.name().toLowerCase().replace('_', ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ScanHit(Material material, int dx, int dy, int dz, double distance) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package com.librewiki.coalgov.service;
|
||||||
|
|
||||||
|
import com.librewiki.coalgov.storage.EconomyRepository;
|
||||||
|
import org.bukkit.OfflinePlayer;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public final class EconomyService {
|
||||||
|
private final EconomyRepository repository;
|
||||||
|
private final long startingBalance;
|
||||||
|
|
||||||
|
public EconomyService(EconomyRepository repository, long startingBalance) {
|
||||||
|
this.repository = repository;
|
||||||
|
this.startingBalance = startingBalance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ensurePlayer(OfflinePlayer player) throws SQLException {
|
||||||
|
repository.ensurePlayer(player, startingBalance);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ensureAccount(UUID uuid, String name) throws SQLException {
|
||||||
|
repository.ensureAccount(uuid, name, startingBalance);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long balance(UUID uuid) throws SQLException {
|
||||||
|
return repository.balance(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean deposit(UUID uuid, long amount) throws SQLException {
|
||||||
|
return repository.add(uuid, amount, null, uuid, "deposit");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean withdraw(UUID uuid, long amount) throws SQLException {
|
||||||
|
return repository.subtract(uuid, amount, uuid, null, "withdrawal");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean pay(UUID from, UUID to, long amount) throws SQLException {
|
||||||
|
return repository.transfer(from, to, amount, "payment");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean grant(UUID uuid, long amount) throws SQLException {
|
||||||
|
return repository.add(uuid, amount, null, uuid, "admin_grant");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean credit(UUID uuid, long amount, String reason) throws SQLException {
|
||||||
|
if (amount <= 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return repository.add(uuid, amount, null, uuid, reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean take(UUID uuid, long amount) throws SQLException {
|
||||||
|
return repository.subtract(uuid, amount, uuid, null, "admin_take");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean charge(UUID uuid, long amount, String reason) throws SQLException {
|
||||||
|
return repository.subtract(uuid, amount, uuid, null, reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package com.librewiki.coalgov.service;
|
||||||
|
|
||||||
|
import com.librewiki.coalgov.model.Fine;
|
||||||
|
import com.librewiki.coalgov.storage.GovernmentRepository;
|
||||||
|
import com.librewiki.coalgov.util.CoalMoney;
|
||||||
|
import org.bukkit.configuration.file.FileConfiguration;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public final class GovernmentService {
|
||||||
|
private final GovernmentRepository repository;
|
||||||
|
private final EconomyService economyService;
|
||||||
|
private FileConfiguration config;
|
||||||
|
|
||||||
|
public GovernmentService(GovernmentRepository repository, EconomyService economyService, FileConfiguration config) {
|
||||||
|
this.repository = repository;
|
||||||
|
this.economyService = economyService;
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConfig(FileConfiguration config) {
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long treasuryBalance() throws SQLException {
|
||||||
|
return repository.treasuryBalance();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean grantFromTreasury(UUID target, long amount) throws SQLException {
|
||||||
|
if (amount <= 0L || !repository.spendTreasury(amount)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!economyService.credit(target, amount, "treasury_grant")) {
|
||||||
|
repository.addTreasury(amount);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long marketSaleTax(UUID playerUuid, long gross) throws SQLException {
|
||||||
|
return transactionTax(playerUuid, gross, "government.taxes.market_sale_percent");
|
||||||
|
}
|
||||||
|
|
||||||
|
public long marketPurchaseTax(UUID playerUuid, long gross) throws SQLException {
|
||||||
|
return transactionTax(playerUuid, gross, "government.taxes.market_purchase_percent");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void collectTax(long amount) throws SQLException {
|
||||||
|
repository.addTreasury(amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean taxExempt(UUID uuid) throws SQLException {
|
||||||
|
return repository.taxExempt(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTaxExempt(UUID uuid, boolean exempt) throws SQLException {
|
||||||
|
repository.setTaxExempt(uuid, exempt);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long createFine(UUID playerUuid, UUID issuerUuid, long amount, String reason) throws SQLException {
|
||||||
|
if (amount <= 0L) {
|
||||||
|
return -1L;
|
||||||
|
}
|
||||||
|
return repository.createFine(playerUuid, issuerUuid, amount, reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Fine> unpaidFines(UUID playerUuid) throws SQLException {
|
||||||
|
return repository.unpaidFines(playerUuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PayFineResult payFine(UUID playerUuid, long id) throws SQLException {
|
||||||
|
Optional<Fine> fine = repository.findUnpaidFine(playerUuid, id);
|
||||||
|
if (fine.isEmpty()) {
|
||||||
|
return PayFineResult.fail("No unpaid fine with that id.");
|
||||||
|
}
|
||||||
|
return pay(playerUuid, fine.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
public PayFineResult payAllFines(UUID playerUuid) throws SQLException {
|
||||||
|
List<Fine> fines = repository.unpaidFines(playerUuid);
|
||||||
|
if (fines.isEmpty()) {
|
||||||
|
return PayFineResult.fail("You have no unpaid fines.");
|
||||||
|
}
|
||||||
|
long total = fines.stream().mapToLong(Fine::amount).sum();
|
||||||
|
if (economyService.balance(playerUuid) < total) {
|
||||||
|
return PayFineResult.fail("You need " + CoalMoney.format(total) + " to pay all fines.");
|
||||||
|
}
|
||||||
|
long paid = 0L;
|
||||||
|
for (Fine fine : fines) {
|
||||||
|
PayFineResult result = pay(playerUuid, fine);
|
||||||
|
if (!result.success()) {
|
||||||
|
return paid > 0L
|
||||||
|
? PayFineResult.success(paid, "Paid " + CoalMoney.format(paid) + " before one fine failed.")
|
||||||
|
: result;
|
||||||
|
}
|
||||||
|
paid += result.amount();
|
||||||
|
}
|
||||||
|
return PayFineResult.success(paid, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private PayFineResult pay(UUID playerUuid, Fine fine) throws SQLException {
|
||||||
|
if (!economyService.charge(playerUuid, fine.amount(), "fine_payment")) {
|
||||||
|
return PayFineResult.fail("Balance too low.");
|
||||||
|
}
|
||||||
|
if (!repository.markFinePaid(fine.id())) {
|
||||||
|
economyService.credit(playerUuid, fine.amount(), "fine_payment_refund");
|
||||||
|
return PayFineResult.fail("Fine payment failed.");
|
||||||
|
}
|
||||||
|
repository.addTreasury(fine.amount());
|
||||||
|
return PayFineResult.success(fine.amount(), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private long transactionTax(UUID playerUuid, long gross, String configPath) throws SQLException {
|
||||||
|
if (gross <= 0L || repository.taxExempt(playerUuid)) {
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
double percent = Math.max(0.0D, config.getDouble(configPath, 0.0D));
|
||||||
|
if (percent <= 0.0D) {
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
return Math.min(gross, Math.max(0L, Math.round(gross * percent / 100.0D)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public record PayFineResult(boolean success, String message, long amount) {
|
||||||
|
public static PayFineResult fail(String message) {
|
||||||
|
return new PayFineResult(false, message, 0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PayFineResult success(long amount, String message) {
|
||||||
|
return new PayFineResult(true, message, amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
package com.librewiki.coalgov.service;
|
||||||
|
|
||||||
|
import com.librewiki.coalgov.CoalGovPlugin;
|
||||||
|
import com.librewiki.coalgov.model.Claim;
|
||||||
|
import com.librewiki.coalgov.util.CoalMoney;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.block.BlockState;
|
||||||
|
import org.bukkit.configuration.ConfigurationSection;
|
||||||
|
import org.bukkit.inventory.InventoryHolder;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public final class LandAppraisalService {
|
||||||
|
private final CoalGovPlugin plugin;
|
||||||
|
private final ClaimService claimService;
|
||||||
|
|
||||||
|
public LandAppraisalService(CoalGovPlugin plugin, ClaimService claimService) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.claimService = claimService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Appraisal appraise(Claim claim) {
|
||||||
|
World world = plugin.getServer().getWorld(claim.world());
|
||||||
|
if (world == null) {
|
||||||
|
long base = claimService.claimPurchaseCost(claim);
|
||||||
|
return new Appraisal(base, claimService.assessedClaimCost(claim), 0.0D, 0.0D, 0.0D, 0.0D, 0L, Map.of(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<Material, Double> prices = resourcePrices();
|
||||||
|
Map<Material, Long> resources = new LinkedHashMap<>();
|
||||||
|
long solid = 0L;
|
||||||
|
long checked = 0L;
|
||||||
|
double resourceValue = 0.0D;
|
||||||
|
|
||||||
|
for (int x = claim.x1(); x <= claim.x2(); x++) {
|
||||||
|
for (int z = claim.z1(); z <= claim.z2(); z++) {
|
||||||
|
int top = world.getHighestBlockYAt(x, z);
|
||||||
|
for (int y = world.getMinHeight(); y <= top; y++) {
|
||||||
|
Block block = world.getBlockAt(x, y, z);
|
||||||
|
Material type = block.getType();
|
||||||
|
checked++;
|
||||||
|
if (!type.isAir()) {
|
||||||
|
solid++;
|
||||||
|
}
|
||||||
|
resourceValue += addResource(resources, prices, type, 1L);
|
||||||
|
resourceValue += inventoryResourceValue(block, resources, prices);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double intactRatio = checked == 0L ? 1.0D : (double) solid / (double) checked;
|
||||||
|
double minMultiplier = plugin.getConfig().getDouble("claims.resale.min_condition_multiplier", 0.25D);
|
||||||
|
minMultiplier = Math.max(0.0D, Math.min(1.0D, minMultiplier));
|
||||||
|
double conditionMultiplier = minMultiplier + ((1.0D - minMultiplier) * intactRatio);
|
||||||
|
long originalCost = claimService.claimPurchaseCost(claim);
|
||||||
|
long assessedCost = claimService.assessedClaimCost(claim);
|
||||||
|
double conditionValue = originalCost * conditionMultiplier;
|
||||||
|
double depletionCharge = originalCost == 0L ? assessedCost * (1.0D - conditionMultiplier) : 0.0D;
|
||||||
|
long total = Math.round(conditionValue + resourceValue - depletionCharge);
|
||||||
|
return new Appraisal(originalCost, assessedCost, conditionMultiplier, conditionValue, resourceValue, depletionCharge, total, resources, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private double inventoryResourceValue(Block block, Map<Material, Long> resources, Map<Material, Double> prices) {
|
||||||
|
BlockState state = block.getState();
|
||||||
|
if (!(state instanceof InventoryHolder holder)) {
|
||||||
|
return 0.0D;
|
||||||
|
}
|
||||||
|
double value = 0.0D;
|
||||||
|
for (ItemStack item : holder.getInventory().getContents()) {
|
||||||
|
if (item == null || item.getType().isAir()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
value += addResource(resources, prices, item.getType(), item.getAmount());
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double addResource(Map<Material, Long> resources, Map<Material, Double> prices, Material material, long amount) {
|
||||||
|
Double price = prices.get(material);
|
||||||
|
if (price == null || amount <= 0L) {
|
||||||
|
return 0.0D;
|
||||||
|
}
|
||||||
|
resources.merge(material, amount, Long::sum);
|
||||||
|
return CoalMoney.fromCoal(price) * amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<Material, Double> resourcePrices() {
|
||||||
|
Map<Material, Double> prices = defaultResourcePrices();
|
||||||
|
ConfigurationSection section = plugin.getConfig().getConfigurationSection("claims.resale.resource_values");
|
||||||
|
if (section == null) {
|
||||||
|
return prices;
|
||||||
|
}
|
||||||
|
for (String key : section.getKeys(false)) {
|
||||||
|
Material material = Material.matchMaterial(key);
|
||||||
|
if (material != null) {
|
||||||
|
prices.put(material, section.getDouble(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return prices;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<Material, Double> defaultResourcePrices() {
|
||||||
|
Map<Material, Double> prices = new LinkedHashMap<>();
|
||||||
|
prices.put(Material.COBBLESTONE, 0.02D);
|
||||||
|
prices.put(Material.COAL, 1.0D);
|
||||||
|
prices.put(Material.COAL_BLOCK, 9.0D);
|
||||||
|
prices.put(Material.RAW_COPPER, 0.5D);
|
||||||
|
prices.put(Material.COPPER_INGOT, 1.0D);
|
||||||
|
prices.put(Material.RAW_IRON, 1.0D);
|
||||||
|
prices.put(Material.IRON_INGOT, 2.0D);
|
||||||
|
prices.put(Material.RAW_GOLD, 2.0D);
|
||||||
|
prices.put(Material.GOLD_INGOT, 4.0D);
|
||||||
|
prices.put(Material.DIAMOND, 16.0D);
|
||||||
|
prices.put(Material.EMERALD, 12.0D);
|
||||||
|
return prices;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Appraisal(
|
||||||
|
long originalCost,
|
||||||
|
long assessedCost,
|
||||||
|
double conditionMultiplier,
|
||||||
|
double conditionValue,
|
||||||
|
double rawResourceValue,
|
||||||
|
double depletionCharge,
|
||||||
|
long totalValue,
|
||||||
|
Map<Material, Long> resources,
|
||||||
|
boolean worldLoaded
|
||||||
|
) {
|
||||||
|
public long resourceValue() {
|
||||||
|
return Math.max(0L, Math.round(rawResourceValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
public long depletionChargeRounded() {
|
||||||
|
return Math.max(0L, Math.round(depletionCharge));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int conditionPercent() {
|
||||||
|
return (int) Math.round(conditionMultiplier * 100.0D);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package com.librewiki.coalgov.service;
|
||||||
|
|
||||||
|
import com.librewiki.coalgov.model.LandClass;
|
||||||
|
import com.librewiki.coalgov.model.LandRegion;
|
||||||
|
import com.librewiki.coalgov.storage.LandRepository;
|
||||||
|
import com.librewiki.coalgov.util.Cuboid2D;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.configuration.file.FileConfiguration;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public final class LandService {
|
||||||
|
private final LandRepository repository;
|
||||||
|
private FileConfiguration config;
|
||||||
|
|
||||||
|
public LandService(LandRepository repository, FileConfiguration config) {
|
||||||
|
this.repository = repository;
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConfig(FileConfiguration config) {
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<LandRegion> regionAt(Location location) throws SQLException {
|
||||||
|
return repository.findAt(location.getWorld().getName(), location.getBlockX(), location.getBlockZ());
|
||||||
|
}
|
||||||
|
|
||||||
|
public LandClass classAt(Location location) throws SQLException {
|
||||||
|
return regionAt(location).map(LandRegion::landClass).orElse(defaultLandClass());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<LandRegion> findByName(String name) throws SQLException {
|
||||||
|
return repository.findByName(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean create(String name, Cuboid2D area, LandClass landClass) throws SQLException {
|
||||||
|
return repository.create(name, area, landClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean delete(String name) throws SQLException {
|
||||||
|
return repository.delete(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<LandRegion> listAll() throws SQLException {
|
||||||
|
return repository.listAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean blocksPrivateClaim(Cuboid2D area) throws SQLException {
|
||||||
|
return repository.overlaps(
|
||||||
|
area,
|
||||||
|
LandClass.GOVERNMENT,
|
||||||
|
LandClass.PUBLIC,
|
||||||
|
LandClass.MINING_CONCESSION,
|
||||||
|
LandClass.PROTECTED_PRESERVE,
|
||||||
|
LandClass.BORDER_ZONE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LandClass defaultLandClass() {
|
||||||
|
try {
|
||||||
|
return LandClass.valueOf(config.getString("land.default_land_class", "FREEHOLD").toUpperCase());
|
||||||
|
} catch (IllegalArgumentException exception) {
|
||||||
|
return LandClass.FREEHOLD;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,336 @@
|
|||||||
|
package com.librewiki.coalgov.service;
|
||||||
|
|
||||||
|
import com.librewiki.coalgov.CoalGovPlugin;
|
||||||
|
import com.librewiki.coalgov.storage.MarketRepository;
|
||||||
|
import com.librewiki.coalgov.util.CoalMoney;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.configuration.ConfigurationSection;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.inventory.Inventory;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import org.bukkit.inventory.meta.ItemMeta;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public final class MarketService {
|
||||||
|
public static final String TITLE = "Ministry Exchange";
|
||||||
|
private static final int SELL_ALL_SLOT = 4;
|
||||||
|
private final CoalGovPlugin plugin;
|
||||||
|
private final MarketRepository repository;
|
||||||
|
|
||||||
|
public MarketService(CoalGovPlugin plugin, MarketRepository repository) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void open(Player player) throws SQLException {
|
||||||
|
Inventory inventory = Bukkit.createInventory(null, 45, TITLE);
|
||||||
|
inventory.setItem(SELL_ALL_SLOT, button(Material.EMERALD, "&aSell appraised resources", List.of(
|
||||||
|
"&7Sells all priced resources in your inventory.",
|
||||||
|
"&7Price rises when Ministry stock is low."
|
||||||
|
)));
|
||||||
|
int slot = 9;
|
||||||
|
for (Material material : basePrices().keySet()) {
|
||||||
|
if (slot >= inventory.getSize()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Quote quote = quote(material);
|
||||||
|
inventory.setItem(slot++, button(material, "&eBuy " + readable(material), List.of(
|
||||||
|
"&7Click: buy 1",
|
||||||
|
"&7Shift-click: buy 64",
|
||||||
|
"&7Stock: " + quote.stock(),
|
||||||
|
"&7Buy 1: " + CoalMoney.format(buyCost(quote, 1)),
|
||||||
|
"&7Buy 64: " + CoalMoney.format(buyCost(quote, 64)),
|
||||||
|
"&7Sell: " + CoalMoney.format(CoalMoney.fromCoal(quote.sellUnitPrice() * 64.0D)) + " per 64"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
player.openInventory(inventory);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sellAll(Player player) throws SQLException {
|
||||||
|
Map<Material, Double> prices = basePrices();
|
||||||
|
Map<Material, Long> simulatedStock = new LinkedHashMap<>();
|
||||||
|
Map<Material, Long> sold = new LinkedHashMap<>();
|
||||||
|
long grossCredit = 0L;
|
||||||
|
for (ItemStack item : player.getInventory().getStorageContents()) {
|
||||||
|
if (item == null || item.getType().isAir()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Material material = item.getType();
|
||||||
|
Double basePrice = prices.get(material);
|
||||||
|
if (basePrice == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
long stock = simulatedStock.containsKey(material)
|
||||||
|
? simulatedStock.get(material)
|
||||||
|
: repository.stock(material, initialStock(material));
|
||||||
|
grossCredit += sellCredit(material, basePrice, stock, item.getAmount());
|
||||||
|
simulatedStock.put(material, stock + item.getAmount());
|
||||||
|
sold.merge(material, (long) item.getAmount(), Long::sum);
|
||||||
|
}
|
||||||
|
if (grossCredit <= 0L) {
|
||||||
|
player.sendMessage(plugin.messages().text("&cNo appraised resources with enough value found in your inventory."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long tax = plugin.governmentService().marketSaleTax(player.getUniqueId(), grossCredit);
|
||||||
|
long credit = Math.max(0L, grossCredit - tax);
|
||||||
|
plugin.economyService().credit(player.getUniqueId(), credit, "market_sale");
|
||||||
|
plugin.governmentService().collectTax(tax);
|
||||||
|
removeSoldItems(player, sold);
|
||||||
|
for (var entry : sold.entrySet()) {
|
||||||
|
repository.addStock(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
if (tax > 0L) {
|
||||||
|
player.sendMessage(plugin.messages().text("&aSold resources for &f" + CoalMoney.format(credit) + " &aafter &f" + CoalMoney.format(tax) + " &atax."));
|
||||||
|
} else {
|
||||||
|
player.sendMessage(plugin.messages().text("&aSold resources for &f" + CoalMoney.format(credit) + "&a."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void buy(Player player, Material material, int amount) throws SQLException {
|
||||||
|
if (!basePrices().containsKey(material)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (amount <= 0) {
|
||||||
|
player.sendMessage(plugin.messages().text("&cInvalid purchase amount."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Quote quote = quote(material);
|
||||||
|
if (quote.stock() < amount) {
|
||||||
|
player.sendMessage(plugin.messages().text("&cThe Ministry has fewer than " + amount + " " + readable(material) + " in stock."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long cost = buyCost(quote, amount);
|
||||||
|
long tax = plugin.governmentService().marketPurchaseTax(player.getUniqueId(), cost);
|
||||||
|
long total = cost + tax;
|
||||||
|
if (plugin.economyService().balance(player.getUniqueId()) < total) {
|
||||||
|
player.sendMessage(plugin.messages().text("&cYou need " + CoalMoney.format(total) + "."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!hasRoomFor(player, material, amount)) {
|
||||||
|
player.sendMessage(plugin.messages().text("&cYou need more inventory space."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!plugin.economyService().charge(player.getUniqueId(), total, "market_purchase")) {
|
||||||
|
player.sendMessage(plugin.messages().text("&cPurchase failed."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!repository.removeStock(material, amount)) {
|
||||||
|
plugin.economyService().credit(player.getUniqueId(), total, "market_refund");
|
||||||
|
player.sendMessage(plugin.messages().text("&cThe Ministry ran out of stock."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!player.getInventory().addItem(new ItemStack(material, amount)).isEmpty()) {
|
||||||
|
repository.addStock(material, amount);
|
||||||
|
plugin.economyService().credit(player.getUniqueId(), total, "market_refund");
|
||||||
|
player.sendMessage(plugin.messages().text("&cYou need more inventory space."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
plugin.governmentService().collectTax(tax);
|
||||||
|
if (tax > 0L) {
|
||||||
|
player.sendMessage(plugin.messages().text("&aBought " + amount + " " + readable(material) + " for &f" + CoalMoney.format(total) + "&a, including &f" + CoalMoney.format(tax) + " &atax."));
|
||||||
|
} else {
|
||||||
|
player.sendMessage(plugin.messages().text("&aBought " + amount + " " + readable(material) + " for &f" + CoalMoney.format(cost) + "&a."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSellAllSlot(int slot) {
|
||||||
|
return slot == SELL_ALL_SLOT;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<Material, Double> basePrices() {
|
||||||
|
Map<Material, Double> prices = defaultPrices();
|
||||||
|
ConfigurationSection section = plugin.getConfig().getConfigurationSection("market.prices");
|
||||||
|
if (section == null) {
|
||||||
|
section = plugin.getConfig().getConfigurationSection("claims.resale.resource_values");
|
||||||
|
}
|
||||||
|
if (section == null) {
|
||||||
|
return prices;
|
||||||
|
}
|
||||||
|
for (String key : section.getKeys(false)) {
|
||||||
|
Material material = Material.matchMaterial(key);
|
||||||
|
if (material != null) {
|
||||||
|
prices.put(material, section.getDouble(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return prices;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Quote quote(Material material) throws SQLException {
|
||||||
|
double basePrice = basePrices().get(material);
|
||||||
|
long stock = repository.stock(material, initialStock(material));
|
||||||
|
return quote(material, basePrice, stock);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Quote quote(Material material, double basePrice, long stock) {
|
||||||
|
return new Quote(stock, sellUnitPrice(material, basePrice, stock), buyUnitPrice(material, basePrice, stock));
|
||||||
|
}
|
||||||
|
|
||||||
|
private long sellCredit(Material material, double basePrice, long startingStock, int amount) {
|
||||||
|
double total = 0.0D;
|
||||||
|
for (int index = 0; index < amount; index++) {
|
||||||
|
total += sellUnitPrice(material, basePrice, startingStock + index);
|
||||||
|
}
|
||||||
|
return Math.max(1L, CoalMoney.fromCoal(total));
|
||||||
|
}
|
||||||
|
|
||||||
|
private double sellUnitPrice(Material material, double basePrice, long stock) {
|
||||||
|
double target = Math.max(1.0D, targetStock());
|
||||||
|
double multiplier = target / Math.max(1.0D, stock);
|
||||||
|
multiplier = Math.max(minMultiplier(), Math.min(maxMultiplier(), multiplier));
|
||||||
|
return Math.max(coalEquivalentFloor(material), basePrice * multiplier * sellMultiplier());
|
||||||
|
}
|
||||||
|
|
||||||
|
private double buyUnitPrice(Material material, double basePrice, long stock) {
|
||||||
|
double sellPrice = sellUnitPrice(material, basePrice, stock);
|
||||||
|
double target = Math.max(1.0D, targetStock());
|
||||||
|
double multiplier = target / Math.max(1.0D, stock);
|
||||||
|
multiplier = Math.max(minMultiplier(), Math.min(maxMultiplier(), multiplier));
|
||||||
|
return Math.max(sellPrice, basePrice * multiplier * buyMultiplier());
|
||||||
|
}
|
||||||
|
|
||||||
|
private long buyCost(Quote quote, int amount) {
|
||||||
|
return Math.max(1L, CoalMoney.fromCoal(quote.buyUnitPrice() * amount));
|
||||||
|
}
|
||||||
|
|
||||||
|
private double coalEquivalentFloor(Material material) {
|
||||||
|
if (material == Material.COAL || material == Material.CHARCOAL) {
|
||||||
|
return 1.0D;
|
||||||
|
}
|
||||||
|
if (material == Material.COAL_BLOCK) {
|
||||||
|
return 9.0D;
|
||||||
|
}
|
||||||
|
String name = material.name();
|
||||||
|
if (name.endsWith("_LOG") || name.endsWith("_STEM")) {
|
||||||
|
return 1.0D;
|
||||||
|
}
|
||||||
|
if (name.endsWith("_PLANKS")) {
|
||||||
|
return 1.5D / 8.0D;
|
||||||
|
}
|
||||||
|
if (material == Material.STICK) {
|
||||||
|
return 0.5D / 8.0D;
|
||||||
|
}
|
||||||
|
return 0.0D;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasRoomFor(Player player, Material material, int amount) {
|
||||||
|
int remaining = amount;
|
||||||
|
for (ItemStack item : player.getInventory().getStorageContents()) {
|
||||||
|
if (item == null || item.getType().isAir()) {
|
||||||
|
remaining -= material.getMaxStackSize();
|
||||||
|
} else if (item.getType() == material) {
|
||||||
|
remaining -= Math.max(0, item.getMaxStackSize() - item.getAmount());
|
||||||
|
}
|
||||||
|
if (remaining <= 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeSoldItems(Player player, Map<Material, Long> sold) {
|
||||||
|
Map<Material, Long> remainingByMaterial = new LinkedHashMap<>(sold);
|
||||||
|
for (ItemStack item : player.getInventory().getStorageContents()) {
|
||||||
|
if (item == null || item.getType().isAir()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Long remaining = remainingByMaterial.get(item.getType());
|
||||||
|
if (remaining == null || remaining <= 0L) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (remaining >= item.getAmount()) {
|
||||||
|
remainingByMaterial.put(item.getType(), remaining - item.getAmount());
|
||||||
|
item.setAmount(0);
|
||||||
|
} else {
|
||||||
|
item.setAmount((int) (item.getAmount() - remaining));
|
||||||
|
remainingByMaterial.put(item.getType(), 0L);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private long initialStock() {
|
||||||
|
return plugin.getConfig().getLong("market.dynamic.initial_stock", 1024L);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long initialStock(Material material) {
|
||||||
|
return plugin.getConfig().getLong("market.initial_stock." + material.name(), initialStock());
|
||||||
|
}
|
||||||
|
|
||||||
|
private long targetStock() {
|
||||||
|
return plugin.getConfig().getLong("market.dynamic.target_stock", 1024L);
|
||||||
|
}
|
||||||
|
|
||||||
|
private double minMultiplier() {
|
||||||
|
return plugin.getConfig().getDouble("market.dynamic.min_multiplier", 0.35D);
|
||||||
|
}
|
||||||
|
|
||||||
|
private double maxMultiplier() {
|
||||||
|
return plugin.getConfig().getDouble("market.dynamic.max_multiplier", 3.0D);
|
||||||
|
}
|
||||||
|
|
||||||
|
private double sellMultiplier() {
|
||||||
|
return plugin.getConfig().getDouble("market.dynamic.sell_multiplier", 0.85D);
|
||||||
|
}
|
||||||
|
|
||||||
|
private double buyMultiplier() {
|
||||||
|
return plugin.getConfig().getDouble("market.dynamic.buy_multiplier", 1.15D);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ItemStack button(Material material, String name, List<String> lore) {
|
||||||
|
ItemStack item = new ItemStack(material);
|
||||||
|
ItemMeta meta = item.getItemMeta();
|
||||||
|
meta.setDisplayName(plugin.messages().raw(name));
|
||||||
|
meta.setLore(lore.stream().map(plugin.messages()::raw).toList());
|
||||||
|
item.setItemMeta(meta);
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<Material, Double> defaultPrices() {
|
||||||
|
Map<Material, Double> prices = new LinkedHashMap<>();
|
||||||
|
prices.put(Material.DIRT, 0.01D);
|
||||||
|
prices.put(Material.COBBLESTONE, 0.02D);
|
||||||
|
prices.put(Material.SAND, 0.05D);
|
||||||
|
prices.put(Material.GRAVEL, 0.04D);
|
||||||
|
prices.put(Material.OAK_LOG, 0.25D);
|
||||||
|
prices.put(Material.SPRUCE_LOG, 0.25D);
|
||||||
|
prices.put(Material.BIRCH_LOG, 0.25D);
|
||||||
|
prices.put(Material.TORCH, 0.10D);
|
||||||
|
prices.put(Material.COAL, 1.0D);
|
||||||
|
prices.put(Material.COAL_BLOCK, 9.0D);
|
||||||
|
prices.put(Material.RAW_COPPER, 0.5D);
|
||||||
|
prices.put(Material.IRON_INGOT, 2.0D);
|
||||||
|
prices.put(Material.GOLD_INGOT, 4.0D);
|
||||||
|
prices.put(Material.DIAMOND, 16.0D);
|
||||||
|
prices.put(Material.EMERALD, 12.0D);
|
||||||
|
prices.put(Material.WHEAT, 0.35D);
|
||||||
|
prices.put(Material.BREAD, 1.25D);
|
||||||
|
prices.put(Material.APPLE, 0.75D);
|
||||||
|
prices.put(Material.CARROT, 0.25D);
|
||||||
|
prices.put(Material.POTATO, 0.20D);
|
||||||
|
prices.put(Material.BAKED_POTATO, 0.45D);
|
||||||
|
prices.put(Material.BEETROOT, 0.20D);
|
||||||
|
prices.put(Material.MELON_SLICE, 0.15D);
|
||||||
|
prices.put(Material.SWEET_BERRIES, 0.20D);
|
||||||
|
prices.put(Material.GLOW_BERRIES, 0.35D);
|
||||||
|
prices.put(Material.COOKIE, 0.25D);
|
||||||
|
prices.put(Material.PUMPKIN_PIE, 1.0D);
|
||||||
|
prices.put(Material.COOKED_CHICKEN, 1.25D);
|
||||||
|
prices.put(Material.COOKED_PORKCHOP, 1.5D);
|
||||||
|
prices.put(Material.COOKED_BEEF, 1.75D);
|
||||||
|
prices.put(Material.COOKED_MUTTON, 1.5D);
|
||||||
|
prices.put(Material.COOKED_RABBIT, 1.25D);
|
||||||
|
prices.put(Material.COOKED_COD, 1.0D);
|
||||||
|
return prices;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String readable(Material material) {
|
||||||
|
return material.name().toLowerCase().replace('_', ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Quote(long stock, double sellUnitPrice, double buyUnitPrice) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.librewiki.coalgov.service;
|
||||||
|
|
||||||
|
import com.librewiki.coalgov.CoalGovPlugin;
|
||||||
|
import com.librewiki.coalgov.model.Claim;
|
||||||
|
import com.librewiki.coalgov.model.LandClass;
|
||||||
|
import com.librewiki.coalgov.model.LandRegion;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public final class MiningService {
|
||||||
|
private final CoalGovPlugin plugin;
|
||||||
|
private final LandService landService;
|
||||||
|
private final ClaimService claimService;
|
||||||
|
private final PermitService permitService;
|
||||||
|
|
||||||
|
public MiningService(CoalGovPlugin plugin, LandService landService, ClaimService claimService, PermitService permitService) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.landService = landService;
|
||||||
|
this.claimService = claimService;
|
||||||
|
this.permitService = permitService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean canMineCoal(Player player, Location location) throws SQLException {
|
||||||
|
if (plugin.hasAdminBypass(player)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Optional<LandRegion> region = landService.regionAt(location);
|
||||||
|
LandClass landClass = region.map(LandRegion::landClass).orElse(landService.classAt(location));
|
||||||
|
if (landClass == LandClass.FREEHOLD) {
|
||||||
|
Optional<Claim> claim = claimService.claimAt(location);
|
||||||
|
return claim.isPresent() && claim.get().ownerUuid().equals(player.getUniqueId());
|
||||||
|
}
|
||||||
|
if (landClass == LandClass.MINING_CONCESSION) {
|
||||||
|
return region.isPresent() && permitService.hasMiningPermit(player.getUniqueId(), region.get().name());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
package com.librewiki.coalgov.service;
|
||||||
|
|
||||||
|
import com.librewiki.coalgov.CoalGovPlugin;
|
||||||
|
import com.librewiki.coalgov.model.ClaimPoint;
|
||||||
|
import com.librewiki.coalgov.model.CoalNpc;
|
||||||
|
import com.librewiki.coalgov.model.NpcZone;
|
||||||
|
import com.librewiki.coalgov.storage.NpcRepository;
|
||||||
|
import com.librewiki.coalgov.util.CoalMoney;
|
||||||
|
import net.citizensnpcs.api.CitizensAPI;
|
||||||
|
import net.citizensnpcs.api.npc.NPC;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.entity.EntityType;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public final class NpcService {
|
||||||
|
private final CoalGovPlugin plugin;
|
||||||
|
private final NpcRepository repository;
|
||||||
|
private final Random random = new Random();
|
||||||
|
|
||||||
|
public NpcService(CoalGovPlugin plugin, NpcRepository repository) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long createZone(String name, String world, List<ClaimPoint> points) throws SQLException {
|
||||||
|
return repository.createZone(name, world, points);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long createNpc(String type, String name, NpcZone zone, Location location) throws SQLException {
|
||||||
|
NPC npc = CitizensAPI.getNPCRegistry().createNPC(EntityType.VILLAGER, name);
|
||||||
|
npc.spawn(location);
|
||||||
|
npc.setProtected(true);
|
||||||
|
UUID accountUuid = UUID.nameUUIDFromBytes(("coalgov:npc:" + npc.getId()).getBytes(StandardCharsets.UTF_8));
|
||||||
|
plugin.economyService().ensureAccount(accountUuid, "NPC-" + name);
|
||||||
|
return repository.createNpc(npc.getId(), accountUuid, type, name, zone.id(),
|
||||||
|
location.getWorld().getName(), location.getX(), location.getY(), location.getZ());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean removeNpc(long id) throws SQLException {
|
||||||
|
Optional<CoalNpc> coalNpc = repository.findNpc(id);
|
||||||
|
if (coalNpc.isPresent()) {
|
||||||
|
NPC npc = CitizensAPI.getNPCRegistry().getById(coalNpc.get().citizensId());
|
||||||
|
if (npc != null) {
|
||||||
|
npc.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return repository.deleteNpc(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<CoalNpc> findNpc(long id) throws SQLException {
|
||||||
|
return repository.findNpc(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<CoalNpc> findByCitizensId(int citizensId) throws SQLException {
|
||||||
|
return repository.findByCitizensId(citizensId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<NpcZone> findZone(String name) throws SQLException {
|
||||||
|
return repository.findZone(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<CoalNpc> listNpcs() throws SQLException {
|
||||||
|
return repository.listNpcs();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<NpcZone> listZones() throws SQLException {
|
||||||
|
return repository.listZones();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<Material, Long> inventory(long npcId) throws SQLException {
|
||||||
|
return repository.inventory(npcId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addInventory(long npcId, Material material, long amount) throws SQLException {
|
||||||
|
repository.addInventory(npcId, material, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long balance(CoalNpc npc) throws SQLException {
|
||||||
|
return plugin.economyService().balance(npc.accountUuid());
|
||||||
|
}
|
||||||
|
|
||||||
|
public HaggleQuote quote(CoalNpc npc, String mode, Material material, int amount, long offer) throws SQLException {
|
||||||
|
double base = plugin.marketService().basePrices().getOrDefault(material, 1.0D);
|
||||||
|
long fair = Math.max(1L, CoalMoney.fromCoal(base * amount));
|
||||||
|
int maxDiscount = plugin.getConfig().getInt("npc.trader.max_discount_percent", 15);
|
||||||
|
int maxSurcharge = plugin.getConfig().getInt("npc.trader.max_surcharge_percent", 20);
|
||||||
|
if (mode.equalsIgnoreCase("buy")) {
|
||||||
|
long stock = repository.inventoryAmount(npc.id(), material);
|
||||||
|
long listPrice = Math.max(1L, Math.round(fair * 1.10D));
|
||||||
|
long minimum = Math.max(1L, Math.round(listPrice * (100.0D - maxDiscount) / 100.0D));
|
||||||
|
long counter = Math.max(minimum, Math.min(listPrice, offer <= 0L ? listPrice : Math.round((offer + listPrice) / 2.0D)));
|
||||||
|
boolean possible = stock >= amount;
|
||||||
|
boolean accepted = possible && offer >= minimum;
|
||||||
|
return new HaggleQuote(mode.toLowerCase(), material, amount, offer, minimum, listPrice, counter, accepted, possible ? "" : "I do not have enough stock.");
|
||||||
|
}
|
||||||
|
long maximum = Math.max(1L, Math.round(fair * (100.0D + maxSurcharge) / 100.0D));
|
||||||
|
long target = Math.max(1L, Math.round(fair * 0.90D));
|
||||||
|
long counter = Math.min(maximum, Math.max(target, offer <= 0L ? target : Math.round((offer + target) / 2.0D)));
|
||||||
|
boolean possible = balance(npc) >= offer;
|
||||||
|
boolean accepted = possible && offer <= maximum;
|
||||||
|
return new HaggleQuote(mode.toLowerCase(), material, amount, offer, target, maximum, counter, accepted, possible ? "" : "I do not have enough coal.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean completeTrade(Player player, CoalNpc npc, HaggleQuote quote, long finalPrice) throws SQLException {
|
||||||
|
if (quote.mode().equals("buy")) {
|
||||||
|
if (repository.inventoryAmount(npc.id(), quote.material()) < quote.amount()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!plugin.economyService().charge(player.getUniqueId(), finalPrice, "npc_purchase")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
repository.removeInventory(npc.id(), quote.material(), quote.amount());
|
||||||
|
plugin.economyService().credit(npc.accountUuid(), finalPrice, "npc_sale");
|
||||||
|
player.getInventory().addItem(new ItemStack(quote.material(), quote.amount()));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!playerHas(player, quote.material(), quote.amount()) || balance(npc) < finalPrice) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!plugin.economyService().charge(npc.accountUuid(), finalPrice, "npc_purchase")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
removePlayerItems(player, quote.material(), quote.amount());
|
||||||
|
plugin.economyService().credit(player.getUniqueId(), finalPrice, "npc_sale");
|
||||||
|
repository.addInventory(npc.id(), quote.material(), quote.amount());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void tickWorkers() {
|
||||||
|
try {
|
||||||
|
for (CoalNpc coalNpc : repository.listNpcs()) {
|
||||||
|
if (!coalNpc.worker()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
NPC npc = CitizensAPI.getNPCRegistry().getById(coalNpc.citizensId());
|
||||||
|
Optional<NpcZone> zone = repository.findZone(coalNpc.zoneId());
|
||||||
|
if (npc == null || zone.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!npc.isSpawned()) {
|
||||||
|
World world = Bukkit.getWorld(coalNpc.world());
|
||||||
|
if (world != null) {
|
||||||
|
npc.spawn(new Location(world, coalNpc.x(), coalNpc.y(), coalNpc.z()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (npc.isSpawned() && !npc.getNavigator().isNavigating()) {
|
||||||
|
Location target = randomPoint(zone.get());
|
||||||
|
if (target != null) {
|
||||||
|
npc.getNavigator().setTarget(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Material material = Material.matchMaterial(plugin.getConfig().getString("npc.worker.produce_material", "BREAD"));
|
||||||
|
int amount = plugin.getConfig().getInt("npc.worker.produce_amount", 2);
|
||||||
|
if (material != null && amount > 0) {
|
||||||
|
repository.addInventory(coalNpc.id(), material, amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException exception) {
|
||||||
|
plugin.getLogger().warning("NPC worker tick failed: " + exception.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Location randomPoint(NpcZone zone) {
|
||||||
|
World world = Bukkit.getWorld(zone.world());
|
||||||
|
if (world == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (int tries = 0; tries < 32; tries++) {
|
||||||
|
int x = zone.x1() + random.nextInt(Math.max(1, zone.x2() - zone.x1() + 1));
|
||||||
|
int z = zone.z1() + random.nextInt(Math.max(1, zone.z2() - zone.z1() + 1));
|
||||||
|
if (zone.contains(world.getName(), x, z)) {
|
||||||
|
return new Location(world, x + 0.5D, world.getHighestBlockYAt(x, z), z + 0.5D);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Location(world, zone.x1() + 0.5D, world.getHighestBlockYAt(zone.x1(), zone.z1()), zone.z1() + 0.5D);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean playerHas(Player player, Material material, int amount) {
|
||||||
|
int found = 0;
|
||||||
|
for (ItemStack item : player.getInventory().getStorageContents()) {
|
||||||
|
if (item != null && item.getType() == material) {
|
||||||
|
found += item.getAmount();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return found >= amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removePlayerItems(Player player, Material material, int amount) {
|
||||||
|
int remaining = amount;
|
||||||
|
for (ItemStack item : player.getInventory().getStorageContents()) {
|
||||||
|
if (item == null || item.getType() != material) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int take = Math.min(remaining, item.getAmount());
|
||||||
|
item.setAmount(item.getAmount() - take);
|
||||||
|
remaining -= take;
|
||||||
|
if (remaining <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record HaggleQuote(String mode, Material material, int amount, long offer, long floor, long ceiling,
|
||||||
|
long counter, boolean accepted, String reason) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
package com.librewiki.coalgov.service;
|
||||||
|
|
||||||
|
import com.librewiki.coalgov.CoalGovPlugin;
|
||||||
|
import com.librewiki.coalgov.util.CoalMoney;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public final class OpenRouterService {
|
||||||
|
private static final Pattern MESSAGE_PATTERN = Pattern.compile("\"message\"\\s*:\\s*\"((?:\\\\.|[^\"])*)\"");
|
||||||
|
private static final Pattern COUNTER_PATTERN = Pattern.compile("\"counter\"\\s*:\\s*(\\d+)");
|
||||||
|
private static final Pattern ACCEPT_PATTERN = Pattern.compile("\"accept\"\\s*:\\s*(true|false)", Pattern.CASE_INSENSITIVE);
|
||||||
|
private final CoalGovPlugin plugin;
|
||||||
|
private final HttpClient client = HttpClient.newHttpClient();
|
||||||
|
|
||||||
|
public OpenRouterService(CoalGovPlugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CompletableFuture<Optional<HaggleAiResult>> haggle(String prompt, long minCounter, long maxCounter) {
|
||||||
|
if (!plugin.getConfig().getBoolean("npc.ai.enabled", true)) {
|
||||||
|
return CompletableFuture.completedFuture(Optional.empty());
|
||||||
|
}
|
||||||
|
String key = plugin.getConfig().getString("npc.ai.api_key", "");
|
||||||
|
if (key == null || key.isBlank()) {
|
||||||
|
return CompletableFuture.completedFuture(Optional.empty());
|
||||||
|
}
|
||||||
|
String model = plugin.getConfig().getString("npc.ai.model", "openai/gpt-4.1-mini");
|
||||||
|
int timeoutMs = plugin.getConfig().getInt("npc.ai.timeout_ms", 4000);
|
||||||
|
String body = """
|
||||||
|
{"model":"%s","messages":[{"role":"system","content":"You are a Minecraft CoalGov NPC trader. Return only compact JSON with accept boolean, counter integer, and message string. The counter must stay within the provided bounds."},{"role":"user","content":"%s"}],"temperature":0.8}
|
||||||
|
""".formatted(escape(model), escape(prompt));
|
||||||
|
HttpRequest request = HttpRequest.newBuilder(URI.create("https://openrouter.ai/api/v1/chat/completions"))
|
||||||
|
.timeout(Duration.ofMillis(timeoutMs))
|
||||||
|
.header("Authorization", "Bearer " + key)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.POST(HttpRequest.BodyPublishers.ofString(body))
|
||||||
|
.build();
|
||||||
|
return client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
|
||||||
|
.thenApply(response -> parse(response.body(), minCounter, maxCounter))
|
||||||
|
.exceptionally(exception -> Optional.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<HaggleAiResult> parse(String response, long minCounter, long maxCounter) {
|
||||||
|
String content = extractContent(response);
|
||||||
|
Matcher counterMatcher = COUNTER_PATTERN.matcher(content);
|
||||||
|
Matcher acceptMatcher = ACCEPT_PATTERN.matcher(content);
|
||||||
|
Matcher messageMatcher = MESSAGE_PATTERN.matcher(content);
|
||||||
|
if (!counterMatcher.find()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
long counter = Long.parseLong(counterMatcher.group(1));
|
||||||
|
counter = Math.max(minCounter, Math.min(maxCounter, counter));
|
||||||
|
boolean accept = acceptMatcher.find() && Boolean.parseBoolean(acceptMatcher.group(1));
|
||||||
|
String message = messageMatcher.find() ? unescape(messageMatcher.group(1)) : "I can work with " + CoalMoney.format(counter) + ".";
|
||||||
|
return Optional.of(new HaggleAiResult(accept, counter, message));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractContent(String response) {
|
||||||
|
int index = response.indexOf("\"content\"");
|
||||||
|
if (index < 0) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
int start = response.indexOf('"', index + 9);
|
||||||
|
if (start < 0) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
start++;
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
boolean escaped = false;
|
||||||
|
for (int i = start; i < response.length(); i++) {
|
||||||
|
char c = response.charAt(i);
|
||||||
|
if (escaped) {
|
||||||
|
builder.append(c);
|
||||||
|
escaped = false;
|
||||||
|
} else if (c == '\\') {
|
||||||
|
escaped = true;
|
||||||
|
} else if (c == '"') {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
builder.append(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String escape(String value) {
|
||||||
|
return value.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String unescape(String value) {
|
||||||
|
return value.replace("\\\"", "\"").replace("\\n", " ").replace("\\\\", "\\");
|
||||||
|
}
|
||||||
|
|
||||||
|
public record HaggleAiResult(boolean accept, long counter, String message) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package com.librewiki.coalgov.service;
|
||||||
|
|
||||||
|
import com.librewiki.coalgov.model.LandClass;
|
||||||
|
import com.librewiki.coalgov.model.LandRegion;
|
||||||
|
import com.librewiki.coalgov.model.Permit;
|
||||||
|
import com.librewiki.coalgov.model.PermitType;
|
||||||
|
import com.librewiki.coalgov.storage.PermitRepository;
|
||||||
|
import com.librewiki.coalgov.util.CoalMoney;
|
||||||
|
import org.bukkit.configuration.file.FileConfiguration;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public final class PermitService {
|
||||||
|
private final PermitRepository repository;
|
||||||
|
private final LandService landService;
|
||||||
|
private final EconomyService economyService;
|
||||||
|
private FileConfiguration config;
|
||||||
|
|
||||||
|
public PermitService(PermitRepository repository, LandService landService, EconomyService economyService, FileConfiguration config) {
|
||||||
|
this.repository = repository;
|
||||||
|
this.landService = landService;
|
||||||
|
this.economyService = economyService;
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConfig(FileConfiguration config) {
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BuyResult buyMining(UUID owner, String regionName) throws SQLException {
|
||||||
|
Optional<LandRegion> region = landService.findByName(regionName);
|
||||||
|
if (region.isEmpty()) {
|
||||||
|
return BuyResult.fail("No land region named " + regionName + ".");
|
||||||
|
}
|
||||||
|
if (region.get().landClass() != LandClass.MINING_CONCESSION) {
|
||||||
|
return BuyResult.fail("That region is not a mining concession.");
|
||||||
|
}
|
||||||
|
long cost = CoalMoney.fromCoalConfig(config.getDouble("permits.mining.cost", 64.0D));
|
||||||
|
if (!economyService.charge(owner, cost, "permit_purchase")) {
|
||||||
|
return BuyResult.fail("You need " + CoalMoney.format(cost) + " for that permit.");
|
||||||
|
}
|
||||||
|
long expiresAt = System.currentTimeMillis() + config.getLong("permits.mining.duration_hours", 72L) * 60L * 60L * 1000L;
|
||||||
|
long id = repository.create(owner, PermitType.MINING, region.get().name(), expiresAt);
|
||||||
|
return BuyResult.success(id, cost, expiresAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasMiningPermit(UUID owner, String regionName) throws SQLException {
|
||||||
|
return repository.hasActive(owner, PermitType.MINING, regionName, System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Permit> listActive(UUID owner) throws SQLException {
|
||||||
|
return repository.listActive(owner, System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
public record BuyResult(boolean success, String message, long id, long cost, long expiresAt) {
|
||||||
|
public static BuyResult fail(String message) {
|
||||||
|
return new BuyResult(false, message, -1L, 0L, 0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BuyResult success(long id, long cost, long expiresAt) {
|
||||||
|
return new BuyResult(true, "", id, cost, expiresAt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package com.librewiki.coalgov.service;
|
||||||
|
|
||||||
|
import com.librewiki.coalgov.CoalGovPlugin;
|
||||||
|
import com.librewiki.coalgov.model.LandClass;
|
||||||
|
import com.librewiki.coalgov.util.Cuboid2D;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.block.Sign;
|
||||||
|
import org.bukkit.block.data.Rotatable;
|
||||||
|
import org.bukkit.block.sign.Side;
|
||||||
|
import org.bukkit.configuration.ConfigurationSection;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class SpawnGuideService {
|
||||||
|
private final CoalGovPlugin plugin;
|
||||||
|
|
||||||
|
public SpawnGuideService(CoalGovPlugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setup(Player player) throws SQLException {
|
||||||
|
Location center = player.getLocation().toBlockLocation();
|
||||||
|
World world = player.getWorld();
|
||||||
|
world.setSpawnLocation(center);
|
||||||
|
if (plugin.getConfig().getBoolean("spawn.guide.create_government_region", true)) {
|
||||||
|
createSpawnRegion(center);
|
||||||
|
}
|
||||||
|
placeGuide(center);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createSpawnRegion(Location center) throws SQLException {
|
||||||
|
int radius = plugin.getConfig().getInt("spawn.protection_radius", 32);
|
||||||
|
Cuboid2D area = new Cuboid2D(
|
||||||
|
center.getWorld().getName(),
|
||||||
|
center.getBlockX() - radius,
|
||||||
|
center.getBlockZ() - radius,
|
||||||
|
center.getBlockX() + radius,
|
||||||
|
center.getBlockZ() + radius
|
||||||
|
);
|
||||||
|
plugin.landService().delete("spawn");
|
||||||
|
plugin.landService().create("spawn", area, LandClass.GOVERNMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void placeGuide(Location center) {
|
||||||
|
List<String> lines = guideLines();
|
||||||
|
int signs = Math.max(1, (int) Math.ceil(lines.size() / 4.0D));
|
||||||
|
int startX = center.getBlockX() - signs + 1;
|
||||||
|
int z = center.getBlockZ() + 3;
|
||||||
|
for (int i = 0; i < signs; i++) {
|
||||||
|
int x = startX + i * 2;
|
||||||
|
int y = center.getWorld().getHighestBlockYAt(x, z);
|
||||||
|
Block base = center.getWorld().getBlockAt(x, y, z);
|
||||||
|
base.setType(Material.POLISHED_ANDESITE);
|
||||||
|
Block signBlock = center.getWorld().getBlockAt(x, y + 1, z);
|
||||||
|
signBlock.setType(Material.OAK_SIGN);
|
||||||
|
if (signBlock.getBlockData() instanceof Rotatable rotatable) {
|
||||||
|
rotatable.setRotation(org.bukkit.block.BlockFace.SOUTH);
|
||||||
|
signBlock.setBlockData(rotatable);
|
||||||
|
}
|
||||||
|
if (signBlock.getState() instanceof Sign sign) {
|
||||||
|
for (int line = 0; line < 4; line++) {
|
||||||
|
int index = i * 4 + line;
|
||||||
|
sign.getSide(Side.FRONT).setLine(line, index < lines.size() ? lines.get(index) : "");
|
||||||
|
}
|
||||||
|
sign.update(true, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> guideLines() {
|
||||||
|
ConfigurationSection section = plugin.getConfig().getConfigurationSection("spawn.guide");
|
||||||
|
List<String> lines = section == null ? List.of() : section.getStringList("lines");
|
||||||
|
if (!lines.isEmpty()) {
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
List<String> defaults = new ArrayList<>();
|
||||||
|
defaults.add("COALGOV BASIN");
|
||||||
|
defaults.add("Free first");
|
||||||
|
defaults.add("homestead:");
|
||||||
|
defaults.add("/claim buy");
|
||||||
|
defaults.add("Use /claim wand");
|
||||||
|
defaults.add("for polygons");
|
||||||
|
defaults.add("Coal seams:");
|
||||||
|
defaults.add("redwood ridges");
|
||||||
|
defaults.add("Permits:");
|
||||||
|
defaults.add("/permit buy");
|
||||||
|
defaults.add("mining <region>");
|
||||||
|
defaults.add("/market trades");
|
||||||
|
return defaults;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package com.librewiki.coalgov.service;
|
||||||
|
|
||||||
|
import com.librewiki.coalgov.CoalGovPlugin;
|
||||||
|
import com.librewiki.coalgov.storage.SuperFurnaceRepository;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.NamespacedKey;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import org.bukkit.inventory.ShapedRecipe;
|
||||||
|
import org.bukkit.inventory.meta.ItemMeta;
|
||||||
|
import org.bukkit.persistence.PersistentDataType;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class SuperFurnaceService {
|
||||||
|
private static final int SPEED_MULTIPLIER = 5;
|
||||||
|
private static final int FUEL_MULTIPLIER = 4;
|
||||||
|
|
||||||
|
private final CoalGovPlugin plugin;
|
||||||
|
private final SuperFurnaceRepository repository;
|
||||||
|
private final NamespacedKey itemKey;
|
||||||
|
private final NamespacedKey recipeKey;
|
||||||
|
|
||||||
|
public SuperFurnaceService(CoalGovPlugin plugin, SuperFurnaceRepository repository) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.repository = repository;
|
||||||
|
this.itemKey = new NamespacedKey(plugin, "super_furnace");
|
||||||
|
this.recipeKey = new NamespacedKey(plugin, "super_furnace_recipe");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerRecipe() {
|
||||||
|
Bukkit.removeRecipe(recipeKey);
|
||||||
|
ShapedRecipe recipe = new ShapedRecipe(recipeKey, item());
|
||||||
|
recipe.shape("SSS", "SFS", "SSS");
|
||||||
|
recipe.setIngredient('S', Material.STONE);
|
||||||
|
recipe.setIngredient('F', Material.FURNACE);
|
||||||
|
Bukkit.addRecipe(recipe);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ItemStack item() {
|
||||||
|
ItemStack item = new ItemStack(Material.FURNACE);
|
||||||
|
ItemMeta meta = item.getItemMeta();
|
||||||
|
meta.setDisplayName(plugin.messages().raw("&6CoalGov Super Furnace"));
|
||||||
|
meta.setLore(List.of(
|
||||||
|
plugin.messages().raw("&75x smelting speed."),
|
||||||
|
plugin.messages().raw("&7Burns fuel 4x faster."),
|
||||||
|
plugin.messages().raw("&7Crafted with stone around a furnace.")
|
||||||
|
));
|
||||||
|
meta.getPersistentDataContainer().set(itemKey, PersistentDataType.BYTE, (byte) 1);
|
||||||
|
item.setItemMeta(meta);
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isItem(ItemStack item) {
|
||||||
|
if (item == null || item.getType() != Material.FURNACE || !item.hasItemMeta()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return item.getItemMeta().getPersistentDataContainer().has(itemKey, PersistentDataType.BYTE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void add(Block block) throws SQLException {
|
||||||
|
repository.add(block.getLocation());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void remove(Block block) throws SQLException {
|
||||||
|
repository.remove(block.getLocation());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSuperFurnace(Block block) throws SQLException {
|
||||||
|
return block.getType() == Material.FURNACE && repository.exists(block.getLocation());
|
||||||
|
}
|
||||||
|
|
||||||
|
public int fasterCookTime(int cookTime) {
|
||||||
|
return Math.max(1, (int) Math.ceil(cookTime / (double) SPEED_MULTIPLIER));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int shorterBurnTime(int burnTime) {
|
||||||
|
return Math.max(1, (int) Math.floor(burnTime / (double) FUEL_MULTIPLIER));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.librewiki.coalgov.service;
|
||||||
|
|
||||||
|
public final class TaxService {
|
||||||
|
public void runMaintenance() {
|
||||||
|
// Placeholder for future lease expiration and recurring tax collection.
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
package com.librewiki.coalgov.storage;
|
||||||
|
|
||||||
|
import com.librewiki.coalgov.model.Claim;
|
||||||
|
import com.librewiki.coalgov.model.ClaimPoint;
|
||||||
|
import com.librewiki.coalgov.util.Cuboid2D;
|
||||||
|
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Statement;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public final class ClaimRepository {
|
||||||
|
private final Database database;
|
||||||
|
|
||||||
|
public ClaimRepository(Database database) {
|
||||||
|
this.database = database;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long create(UUID owner, Cuboid2D area, String claimType, Long expiresAt) throws SQLException {
|
||||||
|
return create(owner, area, claimType, expiresAt, -1L, List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
public long create(UUID owner, Cuboid2D area, String claimType, Long expiresAt, long purchaseCost, List<ClaimPoint> vertices) throws SQLException {
|
||||||
|
boolean originalAutoCommit = database.connection().getAutoCommit();
|
||||||
|
database.connection().setAutoCommit(false);
|
||||||
|
try (PreparedStatement insert = database.connection().prepareStatement("""
|
||||||
|
INSERT INTO claims(owner_uuid, world, x1, z1, x2, z2, claim_type, expires_at, tax_due, purchase_cost, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?)
|
||||||
|
""", Statement.RETURN_GENERATED_KEYS)) {
|
||||||
|
insert.setString(1, owner.toString());
|
||||||
|
insert.setString(2, area.world());
|
||||||
|
insert.setInt(3, area.x1());
|
||||||
|
insert.setInt(4, area.z1());
|
||||||
|
insert.setInt(5, area.x2());
|
||||||
|
insert.setInt(6, area.z2());
|
||||||
|
insert.setString(7, claimType);
|
||||||
|
if (expiresAt == null) {
|
||||||
|
insert.setObject(8, null);
|
||||||
|
} else {
|
||||||
|
insert.setLong(8, expiresAt);
|
||||||
|
}
|
||||||
|
insert.setLong(9, purchaseCost);
|
||||||
|
insert.setLong(10, System.currentTimeMillis());
|
||||||
|
insert.executeUpdate();
|
||||||
|
try (ResultSet keys = insert.getGeneratedKeys()) {
|
||||||
|
long id = keys.next() ? keys.getLong(1) : -1L;
|
||||||
|
insertVertices(id, vertices);
|
||||||
|
database.connection().commit();
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
} catch (SQLException exception) {
|
||||||
|
database.connection().rollback();
|
||||||
|
throw exception;
|
||||||
|
} finally {
|
||||||
|
database.connection().setAutoCommit(originalAutoCommit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean deleteOwned(UUID owner, long id) throws SQLException {
|
||||||
|
boolean originalAutoCommit = database.connection().getAutoCommit();
|
||||||
|
database.connection().setAutoCommit(false);
|
||||||
|
try {
|
||||||
|
try (PreparedStatement deleteVertices = database.connection().prepareStatement(
|
||||||
|
"DELETE FROM claim_vertices WHERE claim_id = ?")) {
|
||||||
|
deleteVertices.setLong(1, id);
|
||||||
|
deleteVertices.executeUpdate();
|
||||||
|
}
|
||||||
|
try (PreparedStatement delete = database.connection().prepareStatement(
|
||||||
|
"DELETE FROM claims WHERE id = ? AND owner_uuid = ?")) {
|
||||||
|
delete.setLong(1, id);
|
||||||
|
delete.setString(2, owner.toString());
|
||||||
|
boolean deleted = delete.executeUpdate() == 1;
|
||||||
|
database.connection().commit();
|
||||||
|
return deleted;
|
||||||
|
}
|
||||||
|
} catch (SQLException exception) {
|
||||||
|
database.connection().rollback();
|
||||||
|
throw exception;
|
||||||
|
} finally {
|
||||||
|
database.connection().setAutoCommit(originalAutoCommit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Claim> findAt(String world, int x, int z) throws SQLException {
|
||||||
|
try (PreparedStatement statement = database.connection().prepareStatement("""
|
||||||
|
SELECT * FROM claims
|
||||||
|
WHERE world = ? AND x1 <= ? AND x2 >= ? AND z1 <= ? AND z2 >= ?
|
||||||
|
ORDER BY id DESC
|
||||||
|
""")) {
|
||||||
|
statement.setString(1, world);
|
||||||
|
statement.setInt(2, x);
|
||||||
|
statement.setInt(3, x);
|
||||||
|
statement.setInt(4, z);
|
||||||
|
statement.setInt(5, z);
|
||||||
|
try (ResultSet result = statement.executeQuery()) {
|
||||||
|
while (result.next()) {
|
||||||
|
Claim claim = map(result);
|
||||||
|
if (claim.contains(world, x, z)) {
|
||||||
|
return Optional.of(claim);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Claim> findByOwner(UUID owner) throws SQLException {
|
||||||
|
try (PreparedStatement statement = database.connection().prepareStatement(
|
||||||
|
"SELECT * FROM claims WHERE owner_uuid = ? ORDER BY id")) {
|
||||||
|
statement.setString(1, owner.toString());
|
||||||
|
return list(statement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Claim> listAll() throws SQLException {
|
||||||
|
try (PreparedStatement statement = database.connection().prepareStatement(
|
||||||
|
"SELECT * FROM claims ORDER BY id")) {
|
||||||
|
return list(statement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Claim> findOwnedById(UUID owner, long id) throws SQLException {
|
||||||
|
try (PreparedStatement statement = database.connection().prepareStatement(
|
||||||
|
"SELECT * FROM claims WHERE owner_uuid = ? AND id = ?")) {
|
||||||
|
statement.setString(1, owner.toString());
|
||||||
|
statement.setLong(2, id);
|
||||||
|
try (ResultSet result = statement.executeQuery()) {
|
||||||
|
return result.next() ? Optional.of(map(result)) : Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean transferOwned(UUID owner, long id, UUID newOwner) throws SQLException {
|
||||||
|
try (PreparedStatement statement = database.connection().prepareStatement(
|
||||||
|
"UPDATE claims SET owner_uuid = ? WHERE id = ? AND owner_uuid = ?")) {
|
||||||
|
statement.setString(1, newOwner.toString());
|
||||||
|
statement.setLong(2, id);
|
||||||
|
statement.setString(3, owner.toString());
|
||||||
|
return statement.executeUpdate() == 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean overlaps(Cuboid2D area) throws SQLException {
|
||||||
|
try (PreparedStatement statement = database.connection().prepareStatement("""
|
||||||
|
SELECT 1 FROM claims
|
||||||
|
WHERE world = ? AND x1 <= ? AND x2 >= ? AND z1 <= ? AND z2 >= ?
|
||||||
|
LIMIT 1
|
||||||
|
""")) {
|
||||||
|
statement.setString(1, area.world());
|
||||||
|
statement.setInt(2, area.x2());
|
||||||
|
statement.setInt(3, area.x1());
|
||||||
|
statement.setInt(4, area.z2());
|
||||||
|
statement.setInt(5, area.z1());
|
||||||
|
try (ResultSet result = statement.executeQuery()) {
|
||||||
|
return result.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Claim> list(PreparedStatement statement) throws SQLException {
|
||||||
|
List<Claim> claims = new ArrayList<>();
|
||||||
|
try (ResultSet result = statement.executeQuery()) {
|
||||||
|
while (result.next()) {
|
||||||
|
claims.add(map(result));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return claims;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Claim map(ResultSet result) throws SQLException {
|
||||||
|
long expiresAt = result.getLong("expires_at");
|
||||||
|
Long nullableExpiresAt = result.wasNull() ? null : expiresAt;
|
||||||
|
return new Claim(
|
||||||
|
result.getLong("id"),
|
||||||
|
UUID.fromString(result.getString("owner_uuid")),
|
||||||
|
result.getString("world"),
|
||||||
|
result.getInt("x1"),
|
||||||
|
result.getInt("z1"),
|
||||||
|
result.getInt("x2"),
|
||||||
|
result.getInt("z2"),
|
||||||
|
result.getString("claim_type"),
|
||||||
|
nullableExpiresAt,
|
||||||
|
result.getLong("tax_due"),
|
||||||
|
result.getLong("created_at"),
|
||||||
|
result.getLong("purchase_cost"),
|
||||||
|
vertices(result.getLong("id"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void insertVertices(long claimId, List<ClaimPoint> vertices) throws SQLException {
|
||||||
|
if (claimId <= 0L || vertices == null || vertices.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try (PreparedStatement insert = database.connection().prepareStatement("""
|
||||||
|
INSERT INTO claim_vertices(claim_id, vertex_order, x, z)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
""")) {
|
||||||
|
for (int i = 0; i < vertices.size(); i++) {
|
||||||
|
ClaimPoint point = vertices.get(i);
|
||||||
|
insert.setLong(1, claimId);
|
||||||
|
insert.setInt(2, i);
|
||||||
|
insert.setInt(3, point.x());
|
||||||
|
insert.setInt(4, point.z());
|
||||||
|
insert.addBatch();
|
||||||
|
}
|
||||||
|
insert.executeBatch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ClaimPoint> vertices(long claimId) throws SQLException {
|
||||||
|
List<ClaimPoint> points = new ArrayList<>();
|
||||||
|
try (PreparedStatement statement = database.connection().prepareStatement(
|
||||||
|
"SELECT x, z FROM claim_vertices WHERE claim_id = ? ORDER BY vertex_order")) {
|
||||||
|
statement.setLong(1, claimId);
|
||||||
|
try (ResultSet result = statement.executeQuery()) {
|
||||||
|
while (result.next()) {
|
||||||
|
points.add(new ClaimPoint(result.getInt("x"), result.getInt("z")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,275 @@
|
|||||||
|
package com.librewiki.coalgov.storage;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.DriverManager;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Statement;
|
||||||
|
|
||||||
|
public final class Database {
|
||||||
|
private final File databaseFile;
|
||||||
|
private Connection connection;
|
||||||
|
|
||||||
|
public Database(File dataFolder) {
|
||||||
|
this.databaseFile = new File(dataFolder, "coalgov.db");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void open() throws SQLException {
|
||||||
|
if (connection != null && !connection.isClosed()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
databaseFile.getParentFile().mkdirs();
|
||||||
|
connection = DriverManager.getConnection("jdbc:sqlite:" + databaseFile.getAbsolutePath());
|
||||||
|
try (Statement statement = connection.createStatement()) {
|
||||||
|
statement.execute("PRAGMA foreign_keys = ON");
|
||||||
|
statement.execute("PRAGMA journal_mode = WAL");
|
||||||
|
}
|
||||||
|
initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Connection connection() {
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() {
|
||||||
|
if (connection == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
connection.close();
|
||||||
|
} catch (SQLException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initialize() throws SQLException {
|
||||||
|
try (Statement statement = connection.createStatement()) {
|
||||||
|
statement.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS schema_meta (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL
|
||||||
|
)
|
||||||
|
""");
|
||||||
|
statement.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS players (
|
||||||
|
uuid TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
coal_balance INTEGER NOT NULL DEFAULT 0,
|
||||||
|
created_at INTEGER NOT NULL
|
||||||
|
)
|
||||||
|
""");
|
||||||
|
statement.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS claims (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
owner_uuid TEXT NOT NULL,
|
||||||
|
world TEXT NOT NULL,
|
||||||
|
x1 INTEGER NOT NULL,
|
||||||
|
z1 INTEGER NOT NULL,
|
||||||
|
x2 INTEGER NOT NULL,
|
||||||
|
z2 INTEGER NOT NULL,
|
||||||
|
claim_type TEXT NOT NULL,
|
||||||
|
expires_at INTEGER,
|
||||||
|
tax_due INTEGER NOT NULL DEFAULT 0,
|
||||||
|
purchase_cost INTEGER NOT NULL DEFAULT -1,
|
||||||
|
created_at INTEGER NOT NULL
|
||||||
|
)
|
||||||
|
""");
|
||||||
|
statement.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS claim_vertices (
|
||||||
|
claim_id INTEGER NOT NULL,
|
||||||
|
vertex_order INTEGER NOT NULL,
|
||||||
|
x INTEGER NOT NULL,
|
||||||
|
z INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (claim_id, vertex_order)
|
||||||
|
)
|
||||||
|
""");
|
||||||
|
statement.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS land_regions (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
world TEXT NOT NULL,
|
||||||
|
x1 INTEGER NOT NULL,
|
||||||
|
z1 INTEGER NOT NULL,
|
||||||
|
x2 INTEGER NOT NULL,
|
||||||
|
z2 INTEGER NOT NULL,
|
||||||
|
land_class TEXT NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL
|
||||||
|
)
|
||||||
|
""");
|
||||||
|
statement.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS permits (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
owner_uuid TEXT NOT NULL,
|
||||||
|
permit_type TEXT NOT NULL,
|
||||||
|
region_name TEXT,
|
||||||
|
expires_at INTEGER,
|
||||||
|
created_at INTEGER NOT NULL
|
||||||
|
)
|
||||||
|
""");
|
||||||
|
statement.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS transactions (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
from_uuid TEXT,
|
||||||
|
to_uuid TEXT,
|
||||||
|
amount INTEGER NOT NULL,
|
||||||
|
reason TEXT NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL
|
||||||
|
)
|
||||||
|
""");
|
||||||
|
statement.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS market_stock (
|
||||||
|
material TEXT PRIMARY KEY,
|
||||||
|
stock INTEGER NOT NULL DEFAULT 0,
|
||||||
|
updated_at INTEGER NOT NULL
|
||||||
|
)
|
||||||
|
""");
|
||||||
|
statement.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS treasury (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
balance INTEGER NOT NULL DEFAULT 0
|
||||||
|
)
|
||||||
|
""");
|
||||||
|
statement.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS tax_exemptions (
|
||||||
|
uuid TEXT PRIMARY KEY,
|
||||||
|
exempt INTEGER NOT NULL DEFAULT 0,
|
||||||
|
updated_at INTEGER NOT NULL
|
||||||
|
)
|
||||||
|
""");
|
||||||
|
statement.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS fines (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
player_uuid TEXT NOT NULL,
|
||||||
|
issuer_uuid TEXT,
|
||||||
|
amount INTEGER NOT NULL,
|
||||||
|
reason TEXT NOT NULL,
|
||||||
|
paid_at INTEGER,
|
||||||
|
created_at INTEGER NOT NULL
|
||||||
|
)
|
||||||
|
""");
|
||||||
|
statement.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS super_furnaces (
|
||||||
|
world TEXT NOT NULL,
|
||||||
|
x INTEGER NOT NULL,
|
||||||
|
y INTEGER NOT NULL,
|
||||||
|
z INTEGER NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (world, x, y, z)
|
||||||
|
)
|
||||||
|
""");
|
||||||
|
statement.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS npc_zones (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
world TEXT NOT NULL,
|
||||||
|
x1 INTEGER NOT NULL,
|
||||||
|
z1 INTEGER NOT NULL,
|
||||||
|
x2 INTEGER NOT NULL,
|
||||||
|
z2 INTEGER NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL
|
||||||
|
)
|
||||||
|
""");
|
||||||
|
statement.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS npc_zone_vertices (
|
||||||
|
zone_id INTEGER NOT NULL,
|
||||||
|
vertex_order INTEGER NOT NULL,
|
||||||
|
x INTEGER NOT NULL,
|
||||||
|
z INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (zone_id, vertex_order)
|
||||||
|
)
|
||||||
|
""");
|
||||||
|
statement.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS coalgov_npcs (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
citizens_id INTEGER NOT NULL UNIQUE,
|
||||||
|
account_uuid TEXT NOT NULL UNIQUE,
|
||||||
|
npc_type TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
zone_id INTEGER NOT NULL,
|
||||||
|
world TEXT NOT NULL,
|
||||||
|
x REAL NOT NULL,
|
||||||
|
y REAL NOT NULL,
|
||||||
|
z REAL NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL
|
||||||
|
)
|
||||||
|
""");
|
||||||
|
statement.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS npc_inventory (
|
||||||
|
npc_id INTEGER NOT NULL,
|
||||||
|
material TEXT NOT NULL,
|
||||||
|
amount INTEGER NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (npc_id, material)
|
||||||
|
)
|
||||||
|
""");
|
||||||
|
statement.execute("CREATE INDEX IF NOT EXISTS idx_claims_location ON claims(world, x1, z1, x2, z2)");
|
||||||
|
statement.execute("CREATE INDEX IF NOT EXISTS idx_claim_vertices_claim ON claim_vertices(claim_id)");
|
||||||
|
statement.execute("CREATE INDEX IF NOT EXISTS idx_land_location ON land_regions(world, x1, z1, x2, z2)");
|
||||||
|
statement.execute("CREATE INDEX IF NOT EXISTS idx_permits_owner ON permits(owner_uuid, permit_type, region_name)");
|
||||||
|
statement.execute("CREATE INDEX IF NOT EXISTS idx_fines_player ON fines(player_uuid, paid_at)");
|
||||||
|
statement.execute("CREATE INDEX IF NOT EXISTS idx_coalgov_npcs_citizens ON coalgov_npcs(citizens_id)");
|
||||||
|
statement.execute("CREATE INDEX IF NOT EXISTS idx_npc_zone_vertices_zone ON npc_zone_vertices(zone_id)");
|
||||||
|
}
|
||||||
|
ensureColumn("claims", "purchase_cost", "INTEGER NOT NULL DEFAULT -1");
|
||||||
|
migrateCoalBalancesToCents();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void migrateCoalBalancesToCents() throws SQLException {
|
||||||
|
String version = metaValue("economy_units");
|
||||||
|
if ("coal_cents_v1".equals(version)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
boolean originalAutoCommit = connection.getAutoCommit();
|
||||||
|
connection.setAutoCommit(false);
|
||||||
|
try (Statement statement = connection.createStatement()) {
|
||||||
|
statement.execute("UPDATE players SET coal_balance = coal_balance * 100");
|
||||||
|
statement.execute("UPDATE transactions SET amount = amount * 100");
|
||||||
|
statement.execute("UPDATE treasury SET balance = balance * 100");
|
||||||
|
statement.execute("UPDATE fines SET amount = amount * 100");
|
||||||
|
statement.execute("UPDATE claims SET tax_due = tax_due * 100");
|
||||||
|
statement.execute("UPDATE claims SET purchase_cost = purchase_cost * 100 WHERE purchase_cost >= 0");
|
||||||
|
setMetaValue("economy_units", "coal_cents_v1");
|
||||||
|
connection.commit();
|
||||||
|
} catch (SQLException exception) {
|
||||||
|
connection.rollback();
|
||||||
|
throw exception;
|
||||||
|
} finally {
|
||||||
|
connection.setAutoCommit(originalAutoCommit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String metaValue(String key) throws SQLException {
|
||||||
|
try (PreparedStatement statement = connection.prepareStatement("SELECT value FROM schema_meta WHERE key = ?")) {
|
||||||
|
statement.setString(1, key);
|
||||||
|
try (ResultSet result = statement.executeQuery()) {
|
||||||
|
return result.next() ? result.getString("value") : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setMetaValue(String key, String value) throws SQLException {
|
||||||
|
try (PreparedStatement statement = connection.prepareStatement("""
|
||||||
|
INSERT INTO schema_meta(key, value)
|
||||||
|
VALUES (?, ?)
|
||||||
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
||||||
|
""")) {
|
||||||
|
statement.setString(1, key);
|
||||||
|
statement.setString(2, value);
|
||||||
|
statement.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureColumn(String table, String column, String definition) throws SQLException {
|
||||||
|
try (Statement statement = connection.createStatement();
|
||||||
|
var result = statement.executeQuery("PRAGMA table_info(" + table + ")")) {
|
||||||
|
while (result.next()) {
|
||||||
|
if (column.equalsIgnoreCase(result.getString("name"))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try (Statement statement = connection.createStatement()) {
|
||||||
|
statement.execute("ALTER TABLE " + table + " ADD COLUMN " + column + " " + definition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
package com.librewiki.coalgov.storage;
|
||||||
|
|
||||||
|
import org.bukkit.OfflinePlayer;
|
||||||
|
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public final class EconomyRepository {
|
||||||
|
private final Database database;
|
||||||
|
|
||||||
|
public EconomyRepository(Database database) {
|
||||||
|
this.database = database;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ensurePlayer(OfflinePlayer player, long startingBalance) throws SQLException {
|
||||||
|
String name = player.getName() == null ? player.getUniqueId().toString() : player.getName();
|
||||||
|
ensureAccount(player.getUniqueId(), name, startingBalance);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ensureAccount(UUID uuid, String name, long startingBalance) throws SQLException {
|
||||||
|
try (PreparedStatement insert = database.connection().prepareStatement("""
|
||||||
|
INSERT INTO players(uuid, name, coal_balance, created_at)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
ON CONFLICT(uuid) DO UPDATE SET name = excluded.name
|
||||||
|
""")) {
|
||||||
|
insert.setString(1, uuid.toString());
|
||||||
|
insert.setString(2, name);
|
||||||
|
insert.setLong(3, startingBalance);
|
||||||
|
insert.setLong(4, System.currentTimeMillis());
|
||||||
|
insert.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public long balance(UUID uuid) throws SQLException {
|
||||||
|
try (PreparedStatement statement = database.connection().prepareStatement(
|
||||||
|
"SELECT coal_balance FROM players WHERE uuid = ?")) {
|
||||||
|
statement.setString(1, uuid.toString());
|
||||||
|
try (ResultSet result = statement.executeQuery()) {
|
||||||
|
return result.next() ? result.getLong("coal_balance") : 0L;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean add(UUID uuid, long amount, UUID fromUuid, UUID toUuid, String reason) throws SQLException {
|
||||||
|
if (amount <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try (PreparedStatement update = database.connection().prepareStatement(
|
||||||
|
"UPDATE players SET coal_balance = coal_balance + ? WHERE uuid = ?")) {
|
||||||
|
update.setLong(1, amount);
|
||||||
|
update.setString(2, uuid.toString());
|
||||||
|
if (update.executeUpdate() != 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recordTransaction(fromUuid, toUuid, amount, reason);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean subtract(UUID uuid, long amount, UUID fromUuid, UUID toUuid, String reason) throws SQLException {
|
||||||
|
if (amount <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try (PreparedStatement update = database.connection().prepareStatement("""
|
||||||
|
UPDATE players SET coal_balance = coal_balance - ?
|
||||||
|
WHERE uuid = ? AND coal_balance >= ?
|
||||||
|
""")) {
|
||||||
|
update.setLong(1, amount);
|
||||||
|
update.setString(2, uuid.toString());
|
||||||
|
update.setLong(3, amount);
|
||||||
|
if (update.executeUpdate() != 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recordTransaction(fromUuid, toUuid, amount, reason);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean transfer(UUID from, UUID to, long amount, String reason) throws SQLException {
|
||||||
|
if (amount <= 0 || from.equals(to)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
boolean originalAutoCommit = database.connection().getAutoCommit();
|
||||||
|
database.connection().setAutoCommit(false);
|
||||||
|
try {
|
||||||
|
try (PreparedStatement debit = database.connection().prepareStatement("""
|
||||||
|
UPDATE players SET coal_balance = coal_balance - ?
|
||||||
|
WHERE uuid = ? AND coal_balance >= ?
|
||||||
|
""")) {
|
||||||
|
debit.setLong(1, amount);
|
||||||
|
debit.setString(2, from.toString());
|
||||||
|
debit.setLong(3, amount);
|
||||||
|
if (debit.executeUpdate() != 1) {
|
||||||
|
database.connection().rollback();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try (PreparedStatement credit = database.connection().prepareStatement(
|
||||||
|
"UPDATE players SET coal_balance = coal_balance + ? WHERE uuid = ?")) {
|
||||||
|
credit.setLong(1, amount);
|
||||||
|
credit.setString(2, to.toString());
|
||||||
|
if (credit.executeUpdate() != 1) {
|
||||||
|
database.connection().rollback();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recordTransaction(from, to, amount, reason);
|
||||||
|
database.connection().commit();
|
||||||
|
return true;
|
||||||
|
} catch (SQLException exception) {
|
||||||
|
database.connection().rollback();
|
||||||
|
throw exception;
|
||||||
|
} finally {
|
||||||
|
database.connection().setAutoCommit(originalAutoCommit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void recordTransaction(UUID fromUuid, UUID toUuid, long amount, String reason) throws SQLException {
|
||||||
|
try (PreparedStatement insert = database.connection().prepareStatement("""
|
||||||
|
INSERT INTO transactions(from_uuid, to_uuid, amount, reason, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
""")) {
|
||||||
|
insert.setString(1, fromUuid == null ? null : fromUuid.toString());
|
||||||
|
insert.setString(2, toUuid == null ? null : toUuid.toString());
|
||||||
|
insert.setLong(3, amount);
|
||||||
|
insert.setString(4, reason);
|
||||||
|
insert.setLong(5, System.currentTimeMillis());
|
||||||
|
insert.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
package com.librewiki.coalgov.storage;
|
||||||
|
|
||||||
|
import com.librewiki.coalgov.model.Fine;
|
||||||
|
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Statement;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public final class GovernmentRepository {
|
||||||
|
private static final String TREASURY_ID = "main";
|
||||||
|
private final Database database;
|
||||||
|
|
||||||
|
public GovernmentRepository(Database database) {
|
||||||
|
this.database = database;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long treasuryBalance() throws SQLException {
|
||||||
|
ensureTreasury();
|
||||||
|
try (PreparedStatement statement = database.connection().prepareStatement(
|
||||||
|
"SELECT balance FROM treasury WHERE id = ?")) {
|
||||||
|
statement.setString(1, TREASURY_ID);
|
||||||
|
try (ResultSet result = statement.executeQuery()) {
|
||||||
|
return result.next() ? result.getLong("balance") : 0L;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addTreasury(long amount) throws SQLException {
|
||||||
|
if (amount <= 0L) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try (PreparedStatement statement = database.connection().prepareStatement("""
|
||||||
|
INSERT INTO treasury(id, balance)
|
||||||
|
VALUES (?, ?)
|
||||||
|
ON CONFLICT(id) DO UPDATE SET balance = balance + excluded.balance
|
||||||
|
""")) {
|
||||||
|
statement.setString(1, TREASURY_ID);
|
||||||
|
statement.setLong(2, amount);
|
||||||
|
statement.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean spendTreasury(long amount) throws SQLException {
|
||||||
|
if (amount <= 0L) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ensureTreasury();
|
||||||
|
try (PreparedStatement statement = database.connection().prepareStatement("""
|
||||||
|
UPDATE treasury SET balance = balance - ?
|
||||||
|
WHERE id = ? AND balance >= ?
|
||||||
|
""")) {
|
||||||
|
statement.setLong(1, amount);
|
||||||
|
statement.setString(2, TREASURY_ID);
|
||||||
|
statement.setLong(3, amount);
|
||||||
|
return statement.executeUpdate() == 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTaxExempt(UUID uuid, boolean exempt) throws SQLException {
|
||||||
|
try (PreparedStatement statement = database.connection().prepareStatement("""
|
||||||
|
INSERT INTO tax_exemptions(uuid, exempt, updated_at)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
ON CONFLICT(uuid) DO UPDATE SET exempt = excluded.exempt, updated_at = excluded.updated_at
|
||||||
|
""")) {
|
||||||
|
statement.setString(1, uuid.toString());
|
||||||
|
statement.setInt(2, exempt ? 1 : 0);
|
||||||
|
statement.setLong(3, System.currentTimeMillis());
|
||||||
|
statement.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean taxExempt(UUID uuid) throws SQLException {
|
||||||
|
try (PreparedStatement statement = database.connection().prepareStatement(
|
||||||
|
"SELECT exempt FROM tax_exemptions WHERE uuid = ?")) {
|
||||||
|
statement.setString(1, uuid.toString());
|
||||||
|
try (ResultSet result = statement.executeQuery()) {
|
||||||
|
return result.next() && result.getInt("exempt") == 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public long createFine(UUID playerUuid, UUID issuerUuid, long amount, String reason) throws SQLException {
|
||||||
|
try (PreparedStatement statement = database.connection().prepareStatement("""
|
||||||
|
INSERT INTO fines(player_uuid, issuer_uuid, amount, reason, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
""", Statement.RETURN_GENERATED_KEYS)) {
|
||||||
|
statement.setString(1, playerUuid.toString());
|
||||||
|
statement.setString(2, issuerUuid == null ? null : issuerUuid.toString());
|
||||||
|
statement.setLong(3, amount);
|
||||||
|
statement.setString(4, reason);
|
||||||
|
statement.setLong(5, System.currentTimeMillis());
|
||||||
|
statement.executeUpdate();
|
||||||
|
try (ResultSet keys = statement.getGeneratedKeys()) {
|
||||||
|
return keys.next() ? keys.getLong(1) : -1L;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Fine> unpaidFines(UUID playerUuid) throws SQLException {
|
||||||
|
try (PreparedStatement statement = database.connection().prepareStatement("""
|
||||||
|
SELECT * FROM fines
|
||||||
|
WHERE player_uuid = ? AND paid_at IS NULL
|
||||||
|
ORDER BY id
|
||||||
|
""")) {
|
||||||
|
statement.setString(1, playerUuid.toString());
|
||||||
|
return list(statement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Fine> findUnpaidFine(UUID playerUuid, long id) throws SQLException {
|
||||||
|
try (PreparedStatement statement = database.connection().prepareStatement("""
|
||||||
|
SELECT * FROM fines
|
||||||
|
WHERE player_uuid = ? AND id = ? AND paid_at IS NULL
|
||||||
|
""")) {
|
||||||
|
statement.setString(1, playerUuid.toString());
|
||||||
|
statement.setLong(2, id);
|
||||||
|
try (ResultSet result = statement.executeQuery()) {
|
||||||
|
return result.next() ? Optional.of(map(result)) : Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean markFinePaid(long id) throws SQLException {
|
||||||
|
try (PreparedStatement statement = database.connection().prepareStatement("""
|
||||||
|
UPDATE fines SET paid_at = ?
|
||||||
|
WHERE id = ? AND paid_at IS NULL
|
||||||
|
""")) {
|
||||||
|
statement.setLong(1, System.currentTimeMillis());
|
||||||
|
statement.setLong(2, id);
|
||||||
|
return statement.executeUpdate() == 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureTreasury() throws SQLException {
|
||||||
|
try (PreparedStatement statement = database.connection().prepareStatement(
|
||||||
|
"INSERT INTO treasury(id, balance) VALUES (?, 0) ON CONFLICT(id) DO NOTHING")) {
|
||||||
|
statement.setString(1, TREASURY_ID);
|
||||||
|
statement.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Fine> list(PreparedStatement statement) throws SQLException {
|
||||||
|
List<Fine> fines = new ArrayList<>();
|
||||||
|
try (ResultSet result = statement.executeQuery()) {
|
||||||
|
while (result.next()) {
|
||||||
|
fines.add(map(result));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fines;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Fine map(ResultSet result) throws SQLException {
|
||||||
|
String issuer = result.getString("issuer_uuid");
|
||||||
|
long paidAt = result.getLong("paid_at");
|
||||||
|
boolean paidAtNull = result.wasNull();
|
||||||
|
return new Fine(
|
||||||
|
result.getLong("id"),
|
||||||
|
UUID.fromString(result.getString("player_uuid")),
|
||||||
|
issuer == null ? null : UUID.fromString(issuer),
|
||||||
|
result.getLong("amount"),
|
||||||
|
result.getString("reason"),
|
||||||
|
paidAtNull ? null : paidAt,
|
||||||
|
result.getLong("created_at")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
package com.librewiki.coalgov.storage;
|
||||||
|
|
||||||
|
import com.librewiki.coalgov.model.LandClass;
|
||||||
|
import com.librewiki.coalgov.model.LandRegion;
|
||||||
|
import com.librewiki.coalgov.util.Cuboid2D;
|
||||||
|
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Statement;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public final class LandRepository {
|
||||||
|
private final Database database;
|
||||||
|
|
||||||
|
public LandRepository(Database database) {
|
||||||
|
this.database = database;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean create(String name, Cuboid2D area, LandClass landClass) throws SQLException {
|
||||||
|
try (PreparedStatement insert = database.connection().prepareStatement("""
|
||||||
|
INSERT INTO land_regions(name, world, x1, z1, x2, z2, land_class, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""")) {
|
||||||
|
insert.setString(1, name);
|
||||||
|
insert.setString(2, area.world());
|
||||||
|
insert.setInt(3, area.x1());
|
||||||
|
insert.setInt(4, area.z1());
|
||||||
|
insert.setInt(5, area.x2());
|
||||||
|
insert.setInt(6, area.z2());
|
||||||
|
insert.setString(7, landClass.name());
|
||||||
|
insert.setLong(8, System.currentTimeMillis());
|
||||||
|
return insert.executeUpdate() == 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean delete(String name) throws SQLException {
|
||||||
|
try (PreparedStatement delete = database.connection().prepareStatement(
|
||||||
|
"DELETE FROM land_regions WHERE lower(name) = lower(?)")) {
|
||||||
|
delete.setString(1, name);
|
||||||
|
return delete.executeUpdate() == 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<LandRegion> findByName(String name) throws SQLException {
|
||||||
|
try (PreparedStatement statement = database.connection().prepareStatement(
|
||||||
|
"SELECT * FROM land_regions WHERE lower(name) = lower(?)")) {
|
||||||
|
statement.setString(1, name);
|
||||||
|
try (ResultSet result = statement.executeQuery()) {
|
||||||
|
return result.next() ? Optional.of(map(result)) : Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<LandRegion> findAt(String world, int x, int z) throws SQLException {
|
||||||
|
try (PreparedStatement statement = database.connection().prepareStatement("""
|
||||||
|
SELECT * FROM land_regions
|
||||||
|
WHERE world = ? AND x1 <= ? AND x2 >= ? AND z1 <= ? AND z2 >= ?
|
||||||
|
ORDER BY id DESC LIMIT 1
|
||||||
|
""")) {
|
||||||
|
statement.setString(1, world);
|
||||||
|
statement.setInt(2, x);
|
||||||
|
statement.setInt(3, x);
|
||||||
|
statement.setInt(4, z);
|
||||||
|
statement.setInt(5, z);
|
||||||
|
try (ResultSet result = statement.executeQuery()) {
|
||||||
|
return result.next() ? Optional.of(map(result)) : Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<LandRegion> listAll() throws SQLException {
|
||||||
|
try (Statement statement = database.connection().createStatement();
|
||||||
|
ResultSet result = statement.executeQuery("SELECT * FROM land_regions ORDER BY name")) {
|
||||||
|
List<LandRegion> regions = new ArrayList<>();
|
||||||
|
while (result.next()) {
|
||||||
|
regions.add(map(result));
|
||||||
|
}
|
||||||
|
return regions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean overlaps(Cuboid2D area, LandClass... blockedClasses) throws SQLException {
|
||||||
|
StringBuilder sql = new StringBuilder("""
|
||||||
|
SELECT land_class FROM land_regions
|
||||||
|
WHERE world = ? AND x1 <= ? AND x2 >= ? AND z1 <= ? AND z2 >= ?
|
||||||
|
""");
|
||||||
|
try (PreparedStatement statement = database.connection().prepareStatement(sql.toString())) {
|
||||||
|
statement.setString(1, area.world());
|
||||||
|
statement.setInt(2, area.x2());
|
||||||
|
statement.setInt(3, area.x1());
|
||||||
|
statement.setInt(4, area.z2());
|
||||||
|
statement.setInt(5, area.z1());
|
||||||
|
try (ResultSet result = statement.executeQuery()) {
|
||||||
|
while (result.next()) {
|
||||||
|
LandClass actual = LandClass.valueOf(result.getString("land_class"));
|
||||||
|
for (LandClass blocked : blockedClasses) {
|
||||||
|
if (actual == blocked) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private LandRegion map(ResultSet result) throws SQLException {
|
||||||
|
return new LandRegion(
|
||||||
|
result.getLong("id"),
|
||||||
|
result.getString("name"),
|
||||||
|
result.getString("world"),
|
||||||
|
result.getInt("x1"),
|
||||||
|
result.getInt("z1"),
|
||||||
|
result.getInt("x2"),
|
||||||
|
result.getInt("z2"),
|
||||||
|
LandClass.valueOf(result.getString("land_class")),
|
||||||
|
result.getLong("created_at")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package com.librewiki.coalgov.storage;
|
||||||
|
|
||||||
|
import org.bukkit.Material;
|
||||||
|
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
public final class MarketRepository {
|
||||||
|
private final Database database;
|
||||||
|
|
||||||
|
public MarketRepository(Database database) {
|
||||||
|
this.database = database;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long stock(Material material, long initialStock) throws SQLException {
|
||||||
|
try (PreparedStatement insert = database.connection().prepareStatement("""
|
||||||
|
INSERT INTO market_stock(material, stock, updated_at)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
ON CONFLICT(material) DO NOTHING
|
||||||
|
""")) {
|
||||||
|
insert.setString(1, material.name());
|
||||||
|
insert.setLong(2, Math.max(0L, initialStock));
|
||||||
|
insert.setLong(3, System.currentTimeMillis());
|
||||||
|
insert.executeUpdate();
|
||||||
|
}
|
||||||
|
try (PreparedStatement select = database.connection().prepareStatement(
|
||||||
|
"SELECT stock FROM market_stock WHERE material = ?")) {
|
||||||
|
select.setString(1, material.name());
|
||||||
|
try (ResultSet result = select.executeQuery()) {
|
||||||
|
return result.next() ? result.getLong("stock") : 0L;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addStock(Material material, long amount) throws SQLException {
|
||||||
|
if (amount <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try (PreparedStatement statement = database.connection().prepareStatement("""
|
||||||
|
INSERT INTO market_stock(material, stock, updated_at)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
ON CONFLICT(material) DO UPDATE SET
|
||||||
|
stock = stock + excluded.stock,
|
||||||
|
updated_at = excluded.updated_at
|
||||||
|
""")) {
|
||||||
|
statement.setString(1, material.name());
|
||||||
|
statement.setLong(2, amount);
|
||||||
|
statement.setLong(3, System.currentTimeMillis());
|
||||||
|
statement.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean removeStock(Material material, long amount) throws SQLException {
|
||||||
|
if (amount <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try (PreparedStatement statement = database.connection().prepareStatement("""
|
||||||
|
UPDATE market_stock SET stock = stock - ?, updated_at = ?
|
||||||
|
WHERE material = ? AND stock >= ?
|
||||||
|
""")) {
|
||||||
|
statement.setLong(1, amount);
|
||||||
|
statement.setLong(2, System.currentTimeMillis());
|
||||||
|
statement.setString(3, material.name());
|
||||||
|
statement.setLong(4, amount);
|
||||||
|
return statement.executeUpdate() == 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
package com.librewiki.coalgov.storage;
|
||||||
|
|
||||||
|
import com.librewiki.coalgov.model.ClaimPoint;
|
||||||
|
import com.librewiki.coalgov.model.CoalNpc;
|
||||||
|
import com.librewiki.coalgov.model.NpcZone;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Statement;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public final class NpcRepository {
|
||||||
|
private final Database database;
|
||||||
|
|
||||||
|
public NpcRepository(Database database) {
|
||||||
|
this.database = database;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long createZone(String name, String world, List<ClaimPoint> vertices) throws SQLException {
|
||||||
|
int x1 = vertices.stream().mapToInt(ClaimPoint::x).min().orElse(0);
|
||||||
|
int x2 = vertices.stream().mapToInt(ClaimPoint::x).max().orElse(0);
|
||||||
|
int z1 = vertices.stream().mapToInt(ClaimPoint::z).min().orElse(0);
|
||||||
|
int z2 = vertices.stream().mapToInt(ClaimPoint::z).max().orElse(0);
|
||||||
|
boolean originalAutoCommit = database.connection().getAutoCommit();
|
||||||
|
database.connection().setAutoCommit(false);
|
||||||
|
try (PreparedStatement insert = database.connection().prepareStatement("""
|
||||||
|
INSERT INTO npc_zones(name, world, x1, z1, x2, z2, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""", Statement.RETURN_GENERATED_KEYS)) {
|
||||||
|
insert.setString(1, name);
|
||||||
|
insert.setString(2, world);
|
||||||
|
insert.setInt(3, x1);
|
||||||
|
insert.setInt(4, z1);
|
||||||
|
insert.setInt(5, x2);
|
||||||
|
insert.setInt(6, z2);
|
||||||
|
insert.setLong(7, System.currentTimeMillis());
|
||||||
|
insert.executeUpdate();
|
||||||
|
try (ResultSet keys = insert.getGeneratedKeys()) {
|
||||||
|
long id = keys.next() ? keys.getLong(1) : -1L;
|
||||||
|
insertVertices(id, vertices);
|
||||||
|
database.connection().commit();
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
} catch (SQLException exception) {
|
||||||
|
database.connection().rollback();
|
||||||
|
throw exception;
|
||||||
|
} finally {
|
||||||
|
database.connection().setAutoCommit(originalAutoCommit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<NpcZone> findZone(String name) throws SQLException {
|
||||||
|
try (PreparedStatement statement = database.connection().prepareStatement(
|
||||||
|
"SELECT * FROM npc_zones WHERE name = ?")) {
|
||||||
|
statement.setString(1, name);
|
||||||
|
try (ResultSet result = statement.executeQuery()) {
|
||||||
|
return result.next() ? Optional.of(mapZone(result)) : Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<NpcZone> findZone(long id) throws SQLException {
|
||||||
|
try (PreparedStatement statement = database.connection().prepareStatement(
|
||||||
|
"SELECT * FROM npc_zones WHERE id = ?")) {
|
||||||
|
statement.setLong(1, id);
|
||||||
|
try (ResultSet result = statement.executeQuery()) {
|
||||||
|
return result.next() ? Optional.of(mapZone(result)) : Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<NpcZone> listZones() throws SQLException {
|
||||||
|
try (PreparedStatement statement = database.connection().prepareStatement("SELECT * FROM npc_zones ORDER BY id")) {
|
||||||
|
List<NpcZone> zones = new ArrayList<>();
|
||||||
|
try (ResultSet result = statement.executeQuery()) {
|
||||||
|
while (result.next()) {
|
||||||
|
zones.add(mapZone(result));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return zones;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public long createNpc(int citizensId, UUID accountUuid, String type, String name, long zoneId,
|
||||||
|
String world, double x, double y, double z) throws SQLException {
|
||||||
|
try (PreparedStatement statement = database.connection().prepareStatement("""
|
||||||
|
INSERT INTO coalgov_npcs(citizens_id, account_uuid, npc_type, name, zone_id, world, x, y, z, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""", Statement.RETURN_GENERATED_KEYS)) {
|
||||||
|
statement.setInt(1, citizensId);
|
||||||
|
statement.setString(2, accountUuid.toString());
|
||||||
|
statement.setString(3, type.toUpperCase());
|
||||||
|
statement.setString(4, name);
|
||||||
|
statement.setLong(5, zoneId);
|
||||||
|
statement.setString(6, world);
|
||||||
|
statement.setDouble(7, x);
|
||||||
|
statement.setDouble(8, y);
|
||||||
|
statement.setDouble(9, z);
|
||||||
|
statement.setLong(10, System.currentTimeMillis());
|
||||||
|
statement.executeUpdate();
|
||||||
|
try (ResultSet keys = statement.getGeneratedKeys()) {
|
||||||
|
return keys.next() ? keys.getLong(1) : -1L;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<CoalNpc> findNpc(long id) throws SQLException {
|
||||||
|
try (PreparedStatement statement = database.connection().prepareStatement("SELECT * FROM coalgov_npcs WHERE id = ?")) {
|
||||||
|
statement.setLong(1, id);
|
||||||
|
try (ResultSet result = statement.executeQuery()) {
|
||||||
|
return result.next() ? Optional.of(mapNpc(result)) : Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<CoalNpc> findByCitizensId(int citizensId) throws SQLException {
|
||||||
|
try (PreparedStatement statement = database.connection().prepareStatement("SELECT * FROM coalgov_npcs WHERE citizens_id = ?")) {
|
||||||
|
statement.setInt(1, citizensId);
|
||||||
|
try (ResultSet result = statement.executeQuery()) {
|
||||||
|
return result.next() ? Optional.of(mapNpc(result)) : Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<CoalNpc> listNpcs() throws SQLException {
|
||||||
|
try (PreparedStatement statement = database.connection().prepareStatement("SELECT * FROM coalgov_npcs ORDER BY id")) {
|
||||||
|
List<CoalNpc> npcs = new ArrayList<>();
|
||||||
|
try (ResultSet result = statement.executeQuery()) {
|
||||||
|
while (result.next()) {
|
||||||
|
npcs.add(mapNpc(result));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return npcs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean deleteNpc(long id) throws SQLException {
|
||||||
|
try (PreparedStatement statement = database.connection().prepareStatement("DELETE FROM coalgov_npcs WHERE id = ?")) {
|
||||||
|
statement.setLong(1, id);
|
||||||
|
return statement.executeUpdate() == 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addInventory(long npcId, Material material, long amount) throws SQLException {
|
||||||
|
if (amount <= 0L) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try (PreparedStatement statement = database.connection().prepareStatement("""
|
||||||
|
INSERT INTO npc_inventory(npc_id, material, amount)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
ON CONFLICT(npc_id, material) DO UPDATE SET amount = amount + excluded.amount
|
||||||
|
""")) {
|
||||||
|
statement.setLong(1, npcId);
|
||||||
|
statement.setString(2, material.name());
|
||||||
|
statement.setLong(3, amount);
|
||||||
|
statement.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean removeInventory(long npcId, Material material, long amount) throws SQLException {
|
||||||
|
if (amount <= 0L) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try (PreparedStatement statement = database.connection().prepareStatement("""
|
||||||
|
UPDATE npc_inventory SET amount = amount - ?
|
||||||
|
WHERE npc_id = ? AND material = ? AND amount >= ?
|
||||||
|
""")) {
|
||||||
|
statement.setLong(1, amount);
|
||||||
|
statement.setLong(2, npcId);
|
||||||
|
statement.setString(3, material.name());
|
||||||
|
statement.setLong(4, amount);
|
||||||
|
return statement.executeUpdate() == 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public long inventoryAmount(long npcId, Material material) throws SQLException {
|
||||||
|
try (PreparedStatement statement = database.connection().prepareStatement(
|
||||||
|
"SELECT amount FROM npc_inventory WHERE npc_id = ? AND material = ?")) {
|
||||||
|
statement.setLong(1, npcId);
|
||||||
|
statement.setString(2, material.name());
|
||||||
|
try (ResultSet result = statement.executeQuery()) {
|
||||||
|
return result.next() ? result.getLong("amount") : 0L;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<Material, Long> inventory(long npcId) throws SQLException {
|
||||||
|
try (PreparedStatement statement = database.connection().prepareStatement(
|
||||||
|
"SELECT material, amount FROM npc_inventory WHERE npc_id = ? AND amount > 0 ORDER BY material")) {
|
||||||
|
statement.setLong(1, npcId);
|
||||||
|
Map<Material, Long> inventory = new LinkedHashMap<>();
|
||||||
|
try (ResultSet result = statement.executeQuery()) {
|
||||||
|
while (result.next()) {
|
||||||
|
Material material = Material.matchMaterial(result.getString("material"));
|
||||||
|
if (material != null) {
|
||||||
|
inventory.put(material, result.getLong("amount"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return inventory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void insertVertices(long zoneId, List<ClaimPoint> vertices) throws SQLException {
|
||||||
|
try (PreparedStatement insert = database.connection().prepareStatement("""
|
||||||
|
INSERT INTO npc_zone_vertices(zone_id, vertex_order, x, z)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
""")) {
|
||||||
|
for (int index = 0; index < vertices.size(); index++) {
|
||||||
|
ClaimPoint point = vertices.get(index);
|
||||||
|
insert.setLong(1, zoneId);
|
||||||
|
insert.setInt(2, index);
|
||||||
|
insert.setInt(3, point.x());
|
||||||
|
insert.setInt(4, point.z());
|
||||||
|
insert.addBatch();
|
||||||
|
}
|
||||||
|
insert.executeBatch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private NpcZone mapZone(ResultSet result) throws SQLException {
|
||||||
|
long id = result.getLong("id");
|
||||||
|
return new NpcZone(
|
||||||
|
id,
|
||||||
|
result.getString("name"),
|
||||||
|
result.getString("world"),
|
||||||
|
result.getInt("x1"),
|
||||||
|
result.getInt("z1"),
|
||||||
|
result.getInt("x2"),
|
||||||
|
result.getInt("z2"),
|
||||||
|
vertices(id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CoalNpc mapNpc(ResultSet result) throws SQLException {
|
||||||
|
return new CoalNpc(
|
||||||
|
result.getLong("id"),
|
||||||
|
result.getInt("citizens_id"),
|
||||||
|
UUID.fromString(result.getString("account_uuid")),
|
||||||
|
result.getString("npc_type"),
|
||||||
|
result.getString("name"),
|
||||||
|
result.getLong("zone_id"),
|
||||||
|
result.getString("world"),
|
||||||
|
result.getDouble("x"),
|
||||||
|
result.getDouble("y"),
|
||||||
|
result.getDouble("z")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ClaimPoint> vertices(long zoneId) throws SQLException {
|
||||||
|
List<ClaimPoint> points = new ArrayList<>();
|
||||||
|
try (PreparedStatement statement = database.connection().prepareStatement(
|
||||||
|
"SELECT x, z FROM npc_zone_vertices WHERE zone_id = ? ORDER BY vertex_order")) {
|
||||||
|
statement.setLong(1, zoneId);
|
||||||
|
try (ResultSet result = statement.executeQuery()) {
|
||||||
|
while (result.next()) {
|
||||||
|
points.add(new ClaimPoint(result.getInt("x"), result.getInt("z")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package com.librewiki.coalgov.storage;
|
||||||
|
|
||||||
|
import com.librewiki.coalgov.model.Permit;
|
||||||
|
import com.librewiki.coalgov.model.PermitType;
|
||||||
|
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Statement;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public final class PermitRepository {
|
||||||
|
private final Database database;
|
||||||
|
|
||||||
|
public PermitRepository(Database database) {
|
||||||
|
this.database = database;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long create(UUID owner, PermitType permitType, String regionName, Long expiresAt) throws SQLException {
|
||||||
|
try (PreparedStatement insert = database.connection().prepareStatement("""
|
||||||
|
INSERT INTO permits(owner_uuid, permit_type, region_name, expires_at, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
""", Statement.RETURN_GENERATED_KEYS)) {
|
||||||
|
insert.setString(1, owner.toString());
|
||||||
|
insert.setString(2, permitType.name());
|
||||||
|
insert.setString(3, regionName);
|
||||||
|
if (expiresAt == null) {
|
||||||
|
insert.setObject(4, null);
|
||||||
|
} else {
|
||||||
|
insert.setLong(4, expiresAt);
|
||||||
|
}
|
||||||
|
insert.setLong(5, System.currentTimeMillis());
|
||||||
|
insert.executeUpdate();
|
||||||
|
try (ResultSet keys = insert.getGeneratedKeys()) {
|
||||||
|
return keys.next() ? keys.getLong(1) : -1L;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasActive(UUID owner, PermitType permitType, String regionName, long now) throws SQLException {
|
||||||
|
try (PreparedStatement statement = database.connection().prepareStatement("""
|
||||||
|
SELECT 1 FROM permits
|
||||||
|
WHERE owner_uuid = ? AND permit_type = ? AND (region_name IS NULL OR lower(region_name) = lower(?))
|
||||||
|
AND (expires_at IS NULL OR expires_at > ?)
|
||||||
|
LIMIT 1
|
||||||
|
""")) {
|
||||||
|
statement.setString(1, owner.toString());
|
||||||
|
statement.setString(2, permitType.name());
|
||||||
|
statement.setString(3, regionName);
|
||||||
|
statement.setLong(4, now);
|
||||||
|
try (ResultSet result = statement.executeQuery()) {
|
||||||
|
return result.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Permit> listActive(UUID owner, long now) throws SQLException {
|
||||||
|
try (PreparedStatement statement = database.connection().prepareStatement("""
|
||||||
|
SELECT * FROM permits
|
||||||
|
WHERE owner_uuid = ? AND (expires_at IS NULL OR expires_at > ?)
|
||||||
|
ORDER BY id
|
||||||
|
""")) {
|
||||||
|
statement.setString(1, owner.toString());
|
||||||
|
statement.setLong(2, now);
|
||||||
|
List<Permit> permits = new ArrayList<>();
|
||||||
|
try (ResultSet result = statement.executeQuery()) {
|
||||||
|
while (result.next()) {
|
||||||
|
permits.add(map(result));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return permits;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Permit map(ResultSet result) throws SQLException {
|
||||||
|
long expiresAt = result.getLong("expires_at");
|
||||||
|
Long nullableExpiresAt = result.wasNull() ? null : expiresAt;
|
||||||
|
return new Permit(
|
||||||
|
result.getLong("id"),
|
||||||
|
UUID.fromString(result.getString("owner_uuid")),
|
||||||
|
PermitType.valueOf(result.getString("permit_type")),
|
||||||
|
result.getString("region_name"),
|
||||||
|
nullableExpiresAt,
|
||||||
|
result.getLong("created_at")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package com.librewiki.coalgov.storage;
|
||||||
|
|
||||||
|
import org.bukkit.Location;
|
||||||
|
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
public final class SuperFurnaceRepository {
|
||||||
|
private final Database database;
|
||||||
|
|
||||||
|
public SuperFurnaceRepository(Database database) {
|
||||||
|
this.database = database;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void add(Location location) throws SQLException {
|
||||||
|
try (PreparedStatement statement = database.connection().prepareStatement("""
|
||||||
|
INSERT INTO super_furnaces(world, x, y, z, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT(world, x, y, z) DO NOTHING
|
||||||
|
""")) {
|
||||||
|
bindLocation(statement, location);
|
||||||
|
statement.setLong(5, System.currentTimeMillis());
|
||||||
|
statement.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void remove(Location location) throws SQLException {
|
||||||
|
try (PreparedStatement statement = database.connection().prepareStatement("""
|
||||||
|
DELETE FROM super_furnaces
|
||||||
|
WHERE world = ? AND x = ? AND y = ? AND z = ?
|
||||||
|
""")) {
|
||||||
|
bindLocation(statement, location);
|
||||||
|
statement.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean exists(Location location) throws SQLException {
|
||||||
|
try (PreparedStatement statement = database.connection().prepareStatement("""
|
||||||
|
SELECT 1 FROM super_furnaces
|
||||||
|
WHERE world = ? AND x = ? AND y = ? AND z = ?
|
||||||
|
""")) {
|
||||||
|
bindLocation(statement, location);
|
||||||
|
try (var result = statement.executeQuery()) {
|
||||||
|
return result.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void bindLocation(PreparedStatement statement, Location location) throws SQLException {
|
||||||
|
statement.setString(1, location.getWorld().getName());
|
||||||
|
statement.setInt(2, location.getBlockX());
|
||||||
|
statement.setInt(3, location.getBlockY());
|
||||||
|
statement.setInt(4, location.getBlockZ());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package com.librewiki.coalgov.util;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
|
||||||
|
public final class CoalMoney {
|
||||||
|
public static final long CENTS_PER_COAL = 100L;
|
||||||
|
|
||||||
|
private CoalMoney() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long fromCoal(double coal) {
|
||||||
|
return Math.round(coal * CENTS_PER_COAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long fromCoalConfig(double coal) {
|
||||||
|
return Math.max(0L, fromCoal(coal));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String format(long cents) {
|
||||||
|
boolean negative = cents < 0L;
|
||||||
|
long absolute = Math.abs(cents);
|
||||||
|
long coal = absolute / CENTS_PER_COAL;
|
||||||
|
long coalCents = absolute % CENTS_PER_COAL;
|
||||||
|
String prefix = negative ? "-" : "";
|
||||||
|
if (coal > 0L && coalCents > 0L) {
|
||||||
|
return prefix + coal + " coal " + coalCents + "cc";
|
||||||
|
}
|
||||||
|
if (coal > 0L) {
|
||||||
|
return prefix + coal + " coal";
|
||||||
|
}
|
||||||
|
return prefix + coalCents + "cc";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long parsePositive(String raw) {
|
||||||
|
long amount = parse(raw);
|
||||||
|
return amount > 0L ? amount : -1L;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long parse(String raw) {
|
||||||
|
if (raw == null) {
|
||||||
|
return -1L;
|
||||||
|
}
|
||||||
|
String normalized = raw.trim().toLowerCase();
|
||||||
|
if (normalized.isEmpty() || normalized.startsWith("-")) {
|
||||||
|
return -1L;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (normalized.endsWith("cc")) {
|
||||||
|
return Long.parseLong(normalized.substring(0, normalized.length() - 2));
|
||||||
|
}
|
||||||
|
if (normalized.endsWith("c")) {
|
||||||
|
return parseDecimalCoal(normalized.substring(0, normalized.length() - 1));
|
||||||
|
}
|
||||||
|
if (normalized.endsWith("coal")) {
|
||||||
|
return parseDecimalCoal(normalized.substring(0, normalized.length() - 4));
|
||||||
|
}
|
||||||
|
return parseDecimalCoal(normalized);
|
||||||
|
} catch (ArithmeticException | NumberFormatException exception) {
|
||||||
|
return -1L;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long parseDecimalCoal(String raw) {
|
||||||
|
BigDecimal coal = new BigDecimal(raw.trim());
|
||||||
|
BigDecimal cents = coal.multiply(BigDecimal.valueOf(CENTS_PER_COAL));
|
||||||
|
return cents.setScale(0, RoundingMode.HALF_UP).longValueExact();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.librewiki.coalgov.util;
|
||||||
|
|
||||||
|
public record Cuboid2D(String world, int x1, int z1, int x2, int z2) {
|
||||||
|
public Cuboid2D {
|
||||||
|
int minX = Math.min(x1, x2);
|
||||||
|
int maxX = Math.max(x1, x2);
|
||||||
|
int minZ = Math.min(z1, z2);
|
||||||
|
int maxZ = Math.max(z1, z2);
|
||||||
|
x1 = minX;
|
||||||
|
x2 = maxX;
|
||||||
|
z1 = minZ;
|
||||||
|
z2 = maxZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean contains(String worldName, int x, int z) {
|
||||||
|
return world.equals(worldName) && x >= x1 && x <= x2 && z >= z1 && z <= z2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean overlaps(Cuboid2D other) {
|
||||||
|
return world.equals(other.world)
|
||||||
|
&& x1 <= other.x2
|
||||||
|
&& x2 >= other.x1
|
||||||
|
&& z1 <= other.z2
|
||||||
|
&& z2 >= other.z1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long area() {
|
||||||
|
return (long) (x2 - x1 + 1) * (z2 - z1 + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.librewiki.coalgov.util;
|
||||||
|
|
||||||
|
import org.bukkit.ChatColor;
|
||||||
|
import org.bukkit.configuration.file.FileConfiguration;
|
||||||
|
|
||||||
|
public final class Messages {
|
||||||
|
private final FileConfiguration config;
|
||||||
|
|
||||||
|
public Messages(FileConfiguration config) {
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String prefix() {
|
||||||
|
return color(config.getString("messages.prefix", "&8[&6CoalGov&8]&r "));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String text(String message) {
|
||||||
|
return prefix() + color(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String raw(String message) {
|
||||||
|
return color(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
private String color(String message) {
|
||||||
|
return ChatColor.translateAlternateColorCodes('&', message);
|
||||||
|
}
|
||||||
|
}
|
||||||
152
CoalGov/src/main/resources/config.yml
Normal file
152
CoalGov/src/main/resources/config.yml
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
economy:
|
||||||
|
starting_balance: 0
|
||||||
|
deposit_coal: true
|
||||||
|
deposit_coal_blocks: true
|
||||||
|
|
||||||
|
government:
|
||||||
|
taxes:
|
||||||
|
market_sale_percent: 5.0
|
||||||
|
market_purchase_percent: 5.0
|
||||||
|
|
||||||
|
npc:
|
||||||
|
ai:
|
||||||
|
enabled: true
|
||||||
|
api_key: ""
|
||||||
|
model: "openai/gpt-4.1-mini"
|
||||||
|
timeout_ms: 4000
|
||||||
|
trader:
|
||||||
|
max_discount_percent: 15
|
||||||
|
max_surcharge_percent: 20
|
||||||
|
worker:
|
||||||
|
tick_interval_seconds: 20
|
||||||
|
produce_material: BREAD
|
||||||
|
produce_amount: 2
|
||||||
|
|
||||||
|
claims:
|
||||||
|
resale:
|
||||||
|
min_condition_multiplier: 0.25
|
||||||
|
resource_values:
|
||||||
|
COBBLESTONE: 0.02
|
||||||
|
COAL: 1.0
|
||||||
|
COAL_BLOCK: 9.0
|
||||||
|
RAW_COPPER: 0.5
|
||||||
|
COPPER_INGOT: 1.0
|
||||||
|
RAW_IRON: 1.0
|
||||||
|
IRON_INGOT: 2.0
|
||||||
|
RAW_GOLD: 2.0
|
||||||
|
GOLD_INGOT: 4.0
|
||||||
|
DIAMOND: 32.0
|
||||||
|
EMERALD: 12.0
|
||||||
|
homestead:
|
||||||
|
base_cost: 32
|
||||||
|
cost_per_block: 0.05
|
||||||
|
max_radius: 45
|
||||||
|
first_claim:
|
||||||
|
free: true
|
||||||
|
max_radius: 12
|
||||||
|
industrial:
|
||||||
|
base_cost: 128
|
||||||
|
cost_per_block: 0.15
|
||||||
|
max_radius: 96
|
||||||
|
|
||||||
|
permits:
|
||||||
|
mining:
|
||||||
|
cost: 64
|
||||||
|
duration_hours: 72
|
||||||
|
|
||||||
|
divining_rod:
|
||||||
|
scan_radius: 4
|
||||||
|
scan_depth: 48
|
||||||
|
cooldown_seconds: 5
|
||||||
|
|
||||||
|
market:
|
||||||
|
dynamic:
|
||||||
|
initial_stock: 1024
|
||||||
|
target_stock: 1024
|
||||||
|
min_multiplier: 0.35
|
||||||
|
max_multiplier: 3.0
|
||||||
|
sell_multiplier: 0.85
|
||||||
|
buy_multiplier: 1.15
|
||||||
|
initial_stock:
|
||||||
|
WHEAT: 0
|
||||||
|
BREAD: 0
|
||||||
|
APPLE: 0
|
||||||
|
CARROT: 0
|
||||||
|
POTATO: 0
|
||||||
|
BAKED_POTATO: 0
|
||||||
|
BEETROOT: 0
|
||||||
|
MELON_SLICE: 0
|
||||||
|
SWEET_BERRIES: 0
|
||||||
|
GLOW_BERRIES: 0
|
||||||
|
COOKIE: 0
|
||||||
|
PUMPKIN_PIE: 0
|
||||||
|
COOKED_CHICKEN: 0
|
||||||
|
COOKED_PORKCHOP: 0
|
||||||
|
COOKED_BEEF: 0
|
||||||
|
COOKED_MUTTON: 0
|
||||||
|
COOKED_RABBIT: 0
|
||||||
|
COOKED_COD: 0
|
||||||
|
prices:
|
||||||
|
DIRT: 0.01
|
||||||
|
COBBLESTONE: 0.02
|
||||||
|
SAND: 0.05
|
||||||
|
GRAVEL: 0.04
|
||||||
|
OAK_LOG: 0.25
|
||||||
|
SPRUCE_LOG: 0.25
|
||||||
|
BIRCH_LOG: 0.25
|
||||||
|
TORCH: 0.10
|
||||||
|
COAL: 1.0
|
||||||
|
COAL_BLOCK: 9.0
|
||||||
|
RAW_COPPER: 0.5
|
||||||
|
COPPER_INGOT: 1.0
|
||||||
|
RAW_IRON: 1.0
|
||||||
|
IRON_INGOT: 2.0
|
||||||
|
RAW_GOLD: 2.0
|
||||||
|
GOLD_INGOT: 4.0
|
||||||
|
DIAMOND: 32.0
|
||||||
|
EMERALD: 12.0
|
||||||
|
WHEAT: 0.35
|
||||||
|
BREAD: 1.25
|
||||||
|
APPLE: 0.75
|
||||||
|
CARROT: 0.25
|
||||||
|
POTATO: 0.20
|
||||||
|
BAKED_POTATO: 0.45
|
||||||
|
BEETROOT: 0.20
|
||||||
|
MELON_SLICE: 0.15
|
||||||
|
SWEET_BERRIES: 0.20
|
||||||
|
GLOW_BERRIES: 0.35
|
||||||
|
COOKIE: 0.25
|
||||||
|
PUMPKIN_PIE: 1.0
|
||||||
|
COOKED_CHICKEN: 1.25
|
||||||
|
COOKED_PORKCHOP: 1.5
|
||||||
|
COOKED_BEEF: 1.75
|
||||||
|
COOKED_MUTTON: 1.5
|
||||||
|
COOKED_RABBIT: 1.25
|
||||||
|
COOKED_COD: 1.0
|
||||||
|
|
||||||
|
land:
|
||||||
|
default_land_class: FREEHOLD
|
||||||
|
deny_mining_in_government: true
|
||||||
|
deny_mining_in_protected_preserve: true
|
||||||
|
deny_mining_in_border_zone: true
|
||||||
|
|
||||||
|
spawn:
|
||||||
|
protection_radius: 32
|
||||||
|
guide:
|
||||||
|
create_government_region: true
|
||||||
|
lines:
|
||||||
|
- "COALGOV BASIN"
|
||||||
|
- "Free first"
|
||||||
|
- "homestead:"
|
||||||
|
- "/claim buy"
|
||||||
|
- "Use /claim wand"
|
||||||
|
- "for polygons"
|
||||||
|
- "Coal seams:"
|
||||||
|
- "redwood ridges"
|
||||||
|
- "Permits:"
|
||||||
|
- "/permit buy"
|
||||||
|
- "mining <region>"
|
||||||
|
- "/market trades"
|
||||||
|
|
||||||
|
messages:
|
||||||
|
prefix: "&8[&6CoalGov&8]&r "
|
||||||
70
CoalGov/src/main/resources/plugin.yml
Normal file
70
CoalGov/src/main/resources/plugin.yml
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
name: CoalGov
|
||||||
|
version: 1.0.0
|
||||||
|
main: com.librewiki.coalgov.CoalGovPlugin
|
||||||
|
api-version: '1.21'
|
||||||
|
author: LibreWiki
|
||||||
|
description: Coal-backed land, claims, permits, and mining governance.
|
||||||
|
depend: [Citizens]
|
||||||
|
|
||||||
|
commands:
|
||||||
|
coal:
|
||||||
|
description: CoalGov economy commands.
|
||||||
|
usage: /coal <balance|deposit|withdraw|pay|fines>
|
||||||
|
permission: coalgov.coal
|
||||||
|
claim:
|
||||||
|
description: CoalGov claim commands.
|
||||||
|
usage: /claim <wand|buy|info|list|show|appraise|sell|transfer|abandon>
|
||||||
|
permission: coalgov.claim
|
||||||
|
permit:
|
||||||
|
description: CoalGov permit commands.
|
||||||
|
usage: /permit <buy|list>
|
||||||
|
permission: coalgov.permit
|
||||||
|
land:
|
||||||
|
description: CoalGov land commands.
|
||||||
|
usage: /land <info|create|delete|list>
|
||||||
|
market:
|
||||||
|
description: CoalGov resource market.
|
||||||
|
usage: /market
|
||||||
|
permission: coalgov.market
|
||||||
|
diviningrod:
|
||||||
|
description: Receive a CoalGov divining rod.
|
||||||
|
usage: /diviningrod
|
||||||
|
permission: coalgov.diviningrod
|
||||||
|
police:
|
||||||
|
description: CoalGov police commands.
|
||||||
|
usage: /police <fine>
|
||||||
|
permission: coalgov.police
|
||||||
|
cgnpc:
|
||||||
|
description: CoalGov NPC commands.
|
||||||
|
usage: /cgnpc <zone|create|remove|list|stock|funds|haggle>
|
||||||
|
permission: coalgov.admin
|
||||||
|
coalgov:
|
||||||
|
description: CoalGov admin commands.
|
||||||
|
usage: /coalgov admin <balance|grant|take|reload|spawn|bypass|rod|treasury|tax>
|
||||||
|
permission: coalgov.admin
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
coalgov.admin:
|
||||||
|
description: Allows CoalGov admin commands. Gameplay bypass must be enabled with /coalgov admin bypass on.
|
||||||
|
default: op
|
||||||
|
coalgov.coal:
|
||||||
|
description: Allows coal economy commands.
|
||||||
|
default: true
|
||||||
|
coalgov.claim:
|
||||||
|
description: Allows claim commands.
|
||||||
|
default: true
|
||||||
|
coalgov.permit:
|
||||||
|
description: Allows permit commands.
|
||||||
|
default: true
|
||||||
|
coalgov.land.info:
|
||||||
|
description: Allows land info commands.
|
||||||
|
default: true
|
||||||
|
coalgov.market:
|
||||||
|
description: Allows market trading.
|
||||||
|
default: true
|
||||||
|
coalgov.diviningrod:
|
||||||
|
description: Allows receiving a divining rod.
|
||||||
|
default: true
|
||||||
|
coalgov.police:
|
||||||
|
description: Allows issuing police fines.
|
||||||
|
default: op
|
||||||
115
coalgov-wiki-pages/admin.txt
Normal file
115
coalgov-wiki-pages/admin.txt
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
====== Admin Guide ======
|
||||||
|
|
||||||
|
===== Permissions =====
|
||||||
|
|
||||||
|
* coalgov.admin - admin commands; default op. Gameplay bypass is toggled with ''/coalgov admin bypass on''.
|
||||||
|
* coalgov.coal - coal economy commands; default true.
|
||||||
|
* coalgov.claim - claim commands; default true.
|
||||||
|
* coalgov.permit - permit commands; default true.
|
||||||
|
* coalgov.land.info - land info commands; default true.
|
||||||
|
* coalgov.market - market trading; default true.
|
||||||
|
* coalgov.police - police fine commands; default op.
|
||||||
|
|
||||||
|
===== Creating Land Regions =====
|
||||||
|
|
||||||
|
Stand at the center of the intended area and run:
|
||||||
|
|
||||||
|
/land create <name> <landClass> <radius>
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
/land create ministry-yard GOVERNMENT 24
|
||||||
|
/land create north-pit MINING_CONCESSION 40
|
||||||
|
/land create wall-east BORDER_ZONE 12
|
||||||
|
/land create old-grove PROTECTED_PRESERVE 32
|
||||||
|
|
||||||
|
The command creates a square X/Z region centered on the admin.
|
||||||
|
|
||||||
|
===== Spawn Setup =====
|
||||||
|
|
||||||
|
Stand where the public spawn should be and run:
|
||||||
|
|
||||||
|
/coalgov admin spawn setup
|
||||||
|
|
||||||
|
This sets the world spawn to your current block, creates or replaces a GOVERNMENT land region named ''spawn'', and places guide signs nearby. Normal players cannot modify the spawn guide because unclaimed government land is protected.
|
||||||
|
|
||||||
|
Default spawn guide text can be changed in ''plugins/CoalGov/config.yml'' under ''spawn.guide.lines''.
|
||||||
|
|
||||||
|
===== Starter Homesteads =====
|
||||||
|
|
||||||
|
By default, each player gets one free homestead if it fits within radius 12 and they do not already own a claim:
|
||||||
|
|
||||||
|
claims:
|
||||||
|
homestead:
|
||||||
|
first_claim:
|
||||||
|
free: true
|
||||||
|
max_radius: 12
|
||||||
|
|
||||||
|
Free starter homesteads store purchase value 0. Their resale appraisal can still count resources left on the property, but extracted or damaged land can create a depletion charge.
|
||||||
|
|
||||||
|
===== Economic Controls =====
|
||||||
|
|
||||||
|
Admins can inspect and modify balances:
|
||||||
|
|
||||||
|
/coalgov admin balance <player>
|
||||||
|
/coalgov admin grant <player> <amount>
|
||||||
|
/coalgov admin take <player> <amount>
|
||||||
|
|
||||||
|
Money amounts accept whole coal, decimal coal, or a ''cc'' suffix. For example, ''1.25'' and ''125cc'' are both 1 coal 25cc.
|
||||||
|
|
||||||
|
Admins can inspect and spend the treasury:
|
||||||
|
|
||||||
|
/coalgov admin treasury balance
|
||||||
|
/coalgov admin treasury grant <player> <amount>
|
||||||
|
|
||||||
|
Admins can exempt players from transaction taxes:
|
||||||
|
|
||||||
|
/coalgov admin tax exempt <player> on
|
||||||
|
/coalgov admin tax exempt <player> off
|
||||||
|
/coalgov admin tax exempt <player> status
|
||||||
|
|
||||||
|
Police can issue payable fines:
|
||||||
|
|
||||||
|
/police fine <player> <amount> <reason>
|
||||||
|
|
||||||
|
Admins can grant a divining rod for testing:
|
||||||
|
|
||||||
|
/coalgov admin rod
|
||||||
|
|
||||||
|
===== Admin Bypass Mode =====
|
||||||
|
|
||||||
|
Admins normally play under the same build, marker, and mining restrictions as citizens.
|
||||||
|
|
||||||
|
Use bypass mode only when you need the old god-power behavior:
|
||||||
|
|
||||||
|
/coalgov admin bypass on
|
||||||
|
/coalgov admin bypass off
|
||||||
|
/coalgov admin bypass status
|
||||||
|
|
||||||
|
Bypass mode is per-player and lasts until it is turned off or the plugin/server restarts.
|
||||||
|
|
||||||
|
===== Market Controls =====
|
||||||
|
|
||||||
|
The market is configured in ''plugins/CoalGov/config.yml'' under ''market''. Admins can tune base prices, target stock, the starting stock for new materials, min/max price movement, and the buy/sell spread.
|
||||||
|
|
||||||
|
After editing market config, run:
|
||||||
|
|
||||||
|
/coalgov admin reload
|
||||||
|
|
||||||
|
Existing market stock is stored in SQLite and is not reset by reload.
|
||||||
|
|
||||||
|
===== Reloading Config =====
|
||||||
|
|
||||||
|
After editing plugins/CoalGov/config.yml, run:
|
||||||
|
|
||||||
|
/coalgov admin reload
|
||||||
|
|
||||||
|
===== Server Files =====
|
||||||
|
|
||||||
|
The plugin stores data in:
|
||||||
|
|
||||||
|
plugins/CoalGov/coalgov.db
|
||||||
|
|
||||||
|
The plugin config is:
|
||||||
|
|
||||||
|
plugins/CoalGov/config.yml
|
||||||
74
coalgov-wiki-pages/commands.txt
Normal file
74
coalgov-wiki-pages/commands.txt
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
====== Command Reference ======
|
||||||
|
|
||||||
|
===== Player Economy =====
|
||||||
|
|
||||||
|
* /coal balance - show your coal balance.
|
||||||
|
* /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 fines - list unpaid fines.
|
||||||
|
* /coal fines pay <id|all> - pay one fine or all unpaid fines.
|
||||||
|
|
||||||
|
===== Market =====
|
||||||
|
|
||||||
|
* /market - open the Ministry Exchange resource trading menu.
|
||||||
|
|
||||||
|
===== Claims =====
|
||||||
|
|
||||||
|
* /claim wand - receive a claim wand for polygon selection.
|
||||||
|
* /claim buy <homestead|industrial> <radius> - buy a rectangular claim centered on you.
|
||||||
|
* /claim buy <homestead|industrial> - buy the currently selected wand polygon.
|
||||||
|
* /claim info - show claim and land info at your current location.
|
||||||
|
* /claim list - list your claims.
|
||||||
|
* /claim show <id> - display a temporary particle outline.
|
||||||
|
* /claim appraise [id] - estimate resale value for an owned claim.
|
||||||
|
* /claim sell [id] - sell an owned claim back to the Ministry at appraised value.
|
||||||
|
* /claim transfer <id> <player> - transfer an owned claim to another player.
|
||||||
|
* /claim abandon <id> - delete a claim you own without a refund.
|
||||||
|
|
||||||
|
===== Permits =====
|
||||||
|
|
||||||
|
* /permit buy mining <regionName> - buy a mining permit for a mining concession.
|
||||||
|
* /permit list - list your active permits.
|
||||||
|
|
||||||
|
===== Divining Rod =====
|
||||||
|
|
||||||
|
* /diviningrod - receive a CoalGov Divining Rod.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
===== Land =====
|
||||||
|
|
||||||
|
* /land info - show land region or default land class at your location.
|
||||||
|
* /land create <name> <landClass> <radius> - admin command to create a land region.
|
||||||
|
* /land delete <name> - admin command to delete a land region.
|
||||||
|
* /land list - list land regions.
|
||||||
|
|
||||||
|
===== Admin =====
|
||||||
|
|
||||||
|
* /coalgov admin balance <player> - show a player balance.
|
||||||
|
* /coalgov admin grant <player> <amount> - grant coal or Coal Cents.
|
||||||
|
* /coalgov admin take <player> <amount> - remove coal or Coal Cents.
|
||||||
|
* /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 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.
|
||||||
|
|
||||||
|
===== Police =====
|
||||||
|
|
||||||
|
* /police fine <player> <amount> <reason> - issue a payable fine.
|
||||||
|
|
||||||
|
===== NPCs =====
|
||||||
|
|
||||||
|
* /cgnpc zone create <name> - create an NPC polygon zone from the current claim wand selection.
|
||||||
|
* /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 haggle <id> <buy|sell> <material> <amount> <offer> - negotiate with a trader.
|
||||||
|
* /cgnpc list - list NPC zones and CoalGov NPCs.
|
||||||
71
coalgov-wiki-pages/economy.txt
Normal file
71
coalgov-wiki-pages/economy.txt
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
====== Coal-backed Economy ======
|
||||||
|
|
||||||
|
CoalGov uses Coal Cents for sub-coal transactions. Balances are stored as integer cents, cannot go negative, and display as coal plus ''cc'' when needed. 100cc equals 1 coal.
|
||||||
|
|
||||||
|
===== Conversion =====
|
||||||
|
|
||||||
|
* 1 coal item = 100cc.
|
||||||
|
* 1 coal block = 900cc when deposited.
|
||||||
|
* Plain command amounts are coal, so ''2.5'' means 2 coal 50cc.
|
||||||
|
* A ''cc'' suffix enters cents directly, so ''75cc'' means 75 Coal Cents.
|
||||||
|
* Withdrawals always return coal items, not coal blocks, and must be whole-coal amounts.
|
||||||
|
|
||||||
|
===== Player Balances =====
|
||||||
|
|
||||||
|
Each player has one balance stored by UUID in SQLite. A player row is created automatically when they join the server.
|
||||||
|
|
||||||
|
===== Deposits =====
|
||||||
|
|
||||||
|
Players run:
|
||||||
|
|
||||||
|
/coal deposit
|
||||||
|
|
||||||
|
The plugin scans the player inventory, removes all enabled deposit items, and credits the account. By default, both COAL and COAL_BLOCK are accepted.
|
||||||
|
|
||||||
|
===== Withdrawals =====
|
||||||
|
|
||||||
|
Players run:
|
||||||
|
|
||||||
|
/coal withdraw <amount>
|
||||||
|
|
||||||
|
The command fails if the amount is not positive, the amount is not whole coal, the balance is too low, or the inventory lacks space.
|
||||||
|
|
||||||
|
===== Payments =====
|
||||||
|
|
||||||
|
Players run:
|
||||||
|
|
||||||
|
/coal pay <player> <amount>
|
||||||
|
|
||||||
|
The target player must be online. Payments accept whole coal, decimal coal, or a ''cc'' suffix. The transfer is recorded in the transactions table.
|
||||||
|
|
||||||
|
===== Fines =====
|
||||||
|
|
||||||
|
Players can inspect and pay unpaid fines:
|
||||||
|
|
||||||
|
/coal fines
|
||||||
|
/coal fines pay <id>
|
||||||
|
/coal fines pay all
|
||||||
|
|
||||||
|
Paid fines go into the government treasury.
|
||||||
|
|
||||||
|
===== Resource Market =====
|
||||||
|
|
||||||
|
Players run:
|
||||||
|
|
||||||
|
/market
|
||||||
|
|
||||||
|
The market lets players sell configured resources for coal or Coal Cents and buy stacks from Ministry stock. Prices are dynamic: scarce resources cost more, oversupplied resources pay less, and market stock persists in SQLite.
|
||||||
|
|
||||||
|
Market purchases and sales can charge a configured government tax. Tax-exempt players do not pay transaction tax.
|
||||||
|
|
||||||
|
===== 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.
|
||||||
|
|
||||||
|
===== Transactions =====
|
||||||
|
|
||||||
|
CoalGov records deposits, withdrawals, player payments, admin grants, admin takes, permit purchases, claim purchases, land sales, market sales, market purchases, market refunds, fine payments, and treasury grants in the transactions table.
|
||||||
92
coalgov-wiki-pages/land.txt
Normal file
92
coalgov-wiki-pages/land.txt
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
====== Land, Claims, and Government Regions ======
|
||||||
|
|
||||||
|
CoalGov separates land into government-defined land regions and player-owned claims. Land regions describe what the government says the land is. Claims describe who owns a specific private plot.
|
||||||
|
|
||||||
|
===== Land Classes ======
|
||||||
|
|
||||||
|
* FREEHOLD - private land can exist here. If no land region matches, this is the default unless config changes it.
|
||||||
|
* LEASE - reserved for lease and expiration rules.
|
||||||
|
* GOVERNMENT - state land. Coal mining is denied unless the player has admin permission.
|
||||||
|
* PUBLIC - public government land. It is protected and cannot be privately claimed.
|
||||||
|
* MINING_CONCESSION - coal mining requires a valid mining permit for that named region.
|
||||||
|
* PROTECTED_PRESERVE - protected land. Coal mining is denied unless the player has admin permission.
|
||||||
|
* BORDER_ZONE - border land. Coal mining is denied unless the player has admin permission.
|
||||||
|
|
||||||
|
===== Claim Types =====
|
||||||
|
|
||||||
|
* HOMESTEAD
|
||||||
|
* INDUSTRIAL
|
||||||
|
* MINING_CONCESSION
|
||||||
|
|
||||||
|
The current player commands support buying homestead and industrial claims. Admins define mining concessions as land regions.
|
||||||
|
|
||||||
|
===== Radius Claims =====
|
||||||
|
|
||||||
|
Players can buy a rectangular claim centered on their current location:
|
||||||
|
|
||||||
|
/claim buy <homestead|industrial> <radius>
|
||||||
|
|
||||||
|
The plugin rejects overlapping claims and land classes that cannot be privately claimed, such as government land, public land, protected preserves, border zones, and mining concessions.
|
||||||
|
|
||||||
|
Claim cost comes from config:
|
||||||
|
|
||||||
|
cost = base_cost + area * cost_per_block
|
||||||
|
|
||||||
|
===== Starter Homestead =====
|
||||||
|
|
||||||
|
Each player can receive one small starter homestead for free if they do not already own a claim. By default, the free starter size is radius 12, or a 25x25 block square. Polygon starter claims are also free if their bounding box fits inside that size.
|
||||||
|
|
||||||
|
Free starter land cannot be flipped for land value. CoalGov stores the actual purchase price on the claim. A free starter homestead has purchase value 0, so selling it refunds only resource value left on the property, minus any depletion charge for extracted or damaged land.
|
||||||
|
|
||||||
|
===== Polygon Wand Claims =====
|
||||||
|
|
||||||
|
Players can draw a claim shape with a wand:
|
||||||
|
|
||||||
|
/claim wand
|
||||||
|
|
||||||
|
Right-click blocks to add polygon points. The plugin displays temporary particles between points and temporary gold marker block previews at selected points so the player can see the outline while drawing. Sneak right-click with the wand to clear the current selection.
|
||||||
|
|
||||||
|
After selecting at least three points, buy the claim without a radius:
|
||||||
|
|
||||||
|
/claim buy homestead
|
||||||
|
/claim buy industrial
|
||||||
|
|
||||||
|
The claim uses the polygon itself for ownership checks. The bounding box must still fit within the configured maximum size for the claim type.
|
||||||
|
|
||||||
|
===== Claim Markers =====
|
||||||
|
|
||||||
|
Purchased claims place protected marker blocks at their corners or polygon vertices. These marker blocks are immutable for normal players and make boundaries visible without needing claim IDs.
|
||||||
|
|
||||||
|
Players can also run:
|
||||||
|
|
||||||
|
/claim show <id>
|
||||||
|
|
||||||
|
This displays a temporary particle outline for the claim.
|
||||||
|
|
||||||
|
===== Protected Unclaimed Land =====
|
||||||
|
|
||||||
|
Players can only build or break inside claims they own. Unclaimed land is protected by default. Admins with ''coalgov.admin'' follow the same rules unless they enable bypass mode with ''/coalgov admin bypass on''.
|
||||||
|
|
||||||
|
===== Claim Info =====
|
||||||
|
|
||||||
|
Players run:
|
||||||
|
|
||||||
|
/claim info
|
||||||
|
|
||||||
|
This reports the land class or named land region at the current location, plus any claim at the same spot.
|
||||||
|
|
||||||
|
===== Appraisal and Sale =====
|
||||||
|
|
||||||
|
Players can appraise or sell land without remembering IDs if they stand inside their own claim:
|
||||||
|
|
||||||
|
/claim appraise
|
||||||
|
/claim sell
|
||||||
|
|
||||||
|
The command also accepts an ID when needed:
|
||||||
|
|
||||||
|
/claim appraise <id>
|
||||||
|
/claim sell <id>
|
||||||
|
|
||||||
|
Sale value depends on land condition and configured resources stored on the property. Intact land keeps more of its original purchase value. Extracted or damaged land refunds less. Counted resources on the property add value according to configured resource prices.
|
||||||
|
|
||||||
|
For free starter homesteads, extracted land value can become a charge instead of a refund. This discourages claiming free land, stripping it, and selling it back.
|
||||||
95
coalgov-wiki-pages/market.txt
Normal file
95
coalgov-wiki-pages/market.txt
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
====== Resource Market ======
|
||||||
|
|
||||||
|
CoalGov includes a vanilla-client trading menu at:
|
||||||
|
|
||||||
|
/market
|
||||||
|
|
||||||
|
The menu is a normal server-side inventory GUI, so players do not need Forge, Fabric, client mods, resource packs, or custom launchers.
|
||||||
|
|
||||||
|
===== What It Does =====
|
||||||
|
|
||||||
|
* Players can sell configured resources from their inventory.
|
||||||
|
* Players can buy configured resources one at a time by default.
|
||||||
|
* Click buys 1, and shift-click buys 64.
|
||||||
|
* Buying charges the fractional unit price times the batch size in Coal Cents, with a 1cc minimum for the whole click.
|
||||||
|
* The Ministry keeps a persisted stock count for each traded material.
|
||||||
|
* Selling resources increases Ministry stock.
|
||||||
|
* Buying resources decreases Ministry stock.
|
||||||
|
* Prices move with stock pressure.
|
||||||
|
* Bulk sales are priced against rising simulated stock as the sale is counted.
|
||||||
|
* Coal-equivalent materials have a floor based on coal, charcoal conversion, or fuel value.
|
||||||
|
* Arbitrary/generated materials such as cobblestone do not have a forced 1-coal floor.
|
||||||
|
|
||||||
|
===== Dynamic Prices =====
|
||||||
|
|
||||||
|
Every traded material has a base price in config. The live price is adjusted by comparing current stock to target stock.
|
||||||
|
|
||||||
|
* If stock is low, the multiplier rises and items become more expensive.
|
||||||
|
* If stock is high, the multiplier falls and items become cheaper.
|
||||||
|
* The config has minimum and maximum multipliers to prevent runaway prices.
|
||||||
|
* The buy price is higher than the sell price because the market has a spread.
|
||||||
|
|
||||||
|
This keeps the economy simple while still reacting to player behavior. If everyone sells cobblestone, cobblestone becomes less valuable. If everyone buys out iron, iron becomes more expensive until players sell more into the market.
|
||||||
|
|
||||||
|
When a player sells a full inventory, the whole batch does not receive the first high scarcity price. CoalGov prices the sale as if each sold item is added to Ministry stock before pricing the next item in that batch.
|
||||||
|
|
||||||
|
Food items start with Ministry stock 0 by default, so players must sell food into the market before anyone can buy it back out.
|
||||||
|
|
||||||
|
===== Default Dynamic Settings =====
|
||||||
|
|
||||||
|
market:
|
||||||
|
dynamic:
|
||||||
|
initial_stock: 1024
|
||||||
|
target_stock: 1024
|
||||||
|
min_multiplier: 0.35
|
||||||
|
max_multiplier: 3.0
|
||||||
|
sell_multiplier: 0.85
|
||||||
|
buy_multiplier: 1.15
|
||||||
|
initial_stock:
|
||||||
|
BREAD: 0
|
||||||
|
COOKED_BEEF: 0
|
||||||
|
|
||||||
|
===== Default Traded Resources =====
|
||||||
|
|
||||||
|
* DIRT
|
||||||
|
* COBBLESTONE
|
||||||
|
* SAND
|
||||||
|
* GRAVEL
|
||||||
|
* OAK_LOG
|
||||||
|
* SPRUCE_LOG
|
||||||
|
* BIRCH_LOG
|
||||||
|
* TORCH
|
||||||
|
* COAL
|
||||||
|
* COAL_BLOCK
|
||||||
|
* RAW_COPPER
|
||||||
|
* COPPER_INGOT
|
||||||
|
* RAW_IRON
|
||||||
|
* IRON_INGOT
|
||||||
|
* RAW_GOLD
|
||||||
|
* GOLD_INGOT
|
||||||
|
* DIAMOND
|
||||||
|
* EMERALD
|
||||||
|
* WHEAT
|
||||||
|
* BREAD
|
||||||
|
* APPLE
|
||||||
|
* CARROT
|
||||||
|
* POTATO
|
||||||
|
* BAKED_POTATO
|
||||||
|
* BEETROOT
|
||||||
|
* MELON_SLICE
|
||||||
|
* SWEET_BERRIES
|
||||||
|
* GLOW_BERRIES
|
||||||
|
* COOKIE
|
||||||
|
* PUMPKIN_PIE
|
||||||
|
* COOKED_CHICKEN
|
||||||
|
* COOKED_PORKCHOP
|
||||||
|
* COOKED_BEEF
|
||||||
|
* COOKED_MUTTON
|
||||||
|
* COOKED_RABBIT
|
||||||
|
* COOKED_COD
|
||||||
|
|
||||||
|
Default diamond pricing is 32 coal before market pressure adjustments.
|
||||||
|
|
||||||
|
===== Persistence =====
|
||||||
|
|
||||||
|
Market stock is stored in SQLite in the ''market_stock'' table. Restarting the server does not reset market pressure.
|
||||||
36
coalgov-wiki-pages/start.txt
Normal file
36
coalgov-wiki-pages/start.txt
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
====== CoalGov ======
|
||||||
|
|
||||||
|
CoalGov is a server-side Paper/Bukkit plugin for a vanilla-client Minecraft server. It adds a coal-backed economy, land ownership, mining permits, protected government land, regulated coal mining, claim marker blocks, polygon claim selection, and a resource market inside a walled coal basin.
|
||||||
|
|
||||||
|
The premise is simple: players live inside a walled coal basin where land ownership, mining rights, and political status are controlled by a coal-backed government.
|
||||||
|
|
||||||
|
===== What Players Can Do =====
|
||||||
|
|
||||||
|
* Deposit coal items and coal blocks into a coal-backed balance.
|
||||||
|
* Use Coal Cents for transactions smaller than 1 coal.
|
||||||
|
* Withdraw whole-coal amounts back into coal items.
|
||||||
|
* Pay other players in coal or Coal Cents.
|
||||||
|
* Buy homestead or industrial land claims.
|
||||||
|
* Receive one small starter homestead claim for free.
|
||||||
|
* Draw polygon claims with a claim wand.
|
||||||
|
* See claim boundaries with marker blocks and particles.
|
||||||
|
* Sell land back to the Ministry at appraised value.
|
||||||
|
* Trade appraised resources through a vanilla inventory market.
|
||||||
|
* Inspect land and claim status at their location.
|
||||||
|
* Buy mining permits for government-designated mining concessions.
|
||||||
|
* Mine coal legally when their land or permit status allows it.
|
||||||
|
|
||||||
|
===== Server Compatibility =====
|
||||||
|
|
||||||
|
CoalGov is a Paper plugin, not a client mod. Players use unmodified vanilla Minecraft clients. The server must run Paper or another Bukkit-compatible server implementation.
|
||||||
|
|
||||||
|
===== Quick Links =====
|
||||||
|
|
||||||
|
* [[economy|Coal-backed economy]]
|
||||||
|
* [[market|Resource market]]
|
||||||
|
* [[land|Land classes and claims]]
|
||||||
|
* [[mining|Mining law and permits]]
|
||||||
|
* [[commands|Command reference]]
|
||||||
|
* [[admin|Admin guide]]
|
||||||
|
* [[technical|Technical notes]]
|
||||||
|
* [[testing|Manual test checklist]]
|
||||||
40
coalgov-wiki-pages/technical.txt
Normal file
40
coalgov-wiki-pages/technical.txt
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
====== Technical Notes ======
|
||||||
|
|
||||||
|
===== Plugin Type =====
|
||||||
|
|
||||||
|
CoalGov is a server-side Paper/Bukkit plugin. It is not a Forge mod, Fabric mod, resource pack, datapack, or custom launcher. Vanilla clients can join normally.
|
||||||
|
|
||||||
|
===== Java Package =====
|
||||||
|
|
||||||
|
com.librewiki.coalgov
|
||||||
|
|
||||||
|
===== Architecture =====
|
||||||
|
|
||||||
|
The plugin is split into commands, listeners, models, services, storage repositories, and small utilities. The goal is boring, maintainable Java rather than one giant plugin class.
|
||||||
|
|
||||||
|
===== Persistence =====
|
||||||
|
|
||||||
|
CoalGov uses SQLite at:
|
||||||
|
|
||||||
|
plugins/CoalGov/coalgov.db
|
||||||
|
|
||||||
|
Tables:
|
||||||
|
|
||||||
|
* players
|
||||||
|
* claims
|
||||||
|
* claim_vertices
|
||||||
|
* land_regions
|
||||||
|
* permits
|
||||||
|
* transactions
|
||||||
|
* market_stock
|
||||||
|
|
||||||
|
===== Important Runtime Behavior =====
|
||||||
|
|
||||||
|
* Player rows are created on join.
|
||||||
|
* SQL uses PreparedStatement.
|
||||||
|
* Timestamps are stored as epoch milliseconds.
|
||||||
|
* Database access is synchronous because the target server is tiny.
|
||||||
|
* Money columns are stored as integer Coal Cents. The ''schema_meta'' table records the one-time migration from old whole-coal ledger units.
|
||||||
|
* Admin permission allows admin commands. Coal mining, build protection, and marker protection are bypassed only while the admin has enabled ''/coalgov admin bypass on''.
|
||||||
|
* Polygon claims store their vertices separately from the claim bounding box.
|
||||||
|
* The market stores stock separately from transaction history.
|
||||||
37
coalgov-wiki-pages/testing.txt
Normal file
37
coalgov-wiki-pages/testing.txt
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
====== Manual Test Checklist ======
|
||||||
|
|
||||||
|
Use this checklist after install or upgrade.
|
||||||
|
|
||||||
|
* Server starts without errors.
|
||||||
|
* Database file is created.
|
||||||
|
* Player joining creates a players row.
|
||||||
|
* /coal balance works.
|
||||||
|
* /coal deposit removes coal from inventory and increases balance.
|
||||||
|
* /coal withdraw decreases balance and gives coal items.
|
||||||
|
* /coal pay transfers balance.
|
||||||
|
* /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.
|
||||||
|
* Market prices change after stock changes.
|
||||||
|
* /land create creates a region.
|
||||||
|
* /land info reports current land class.
|
||||||
|
* /claim buy creates a radius claim and charges coal.
|
||||||
|
* A new player's first small homestead costs 0 coal.
|
||||||
|
* Selling a free starter homestead does not refund land value.
|
||||||
|
* Damaging or extracting from a free starter homestead can create a sale charge.
|
||||||
|
* /claim wand gives a claim wand.
|
||||||
|
* Right-clicking blocks with the wand draws a particle polygon.
|
||||||
|
* /claim buy homestead after wand selection creates a polygon claim.
|
||||||
|
* Claim marker blocks appear at rectangle corners or polygon vertices.
|
||||||
|
* Marker blocks cannot be broken by normal players.
|
||||||
|
* Unclaimed land cannot be built on or broken by normal players.
|
||||||
|
* /claim appraise reports land condition and resource value.
|
||||||
|
* /claim sell refunds the appraised value and deletes the claim.
|
||||||
|
* Overlapping claims are rejected.
|
||||||
|
* Mining coal in PROTECTED_PRESERVE is denied.
|
||||||
|
* Mining coal in MINING_CONCESSION without permit is denied.
|
||||||
|
* Buying a mining permit allows mining coal in that concession.
|
||||||
|
* Admin permission alone does not bypass mining, build, or marker restrictions.
|
||||||
|
* /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.
|
||||||
22
docker-compose.yml
Normal file
22
docker-compose.yml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
services:
|
||||||
|
mc:
|
||||||
|
image: itzg/minecraft-server:java21
|
||||||
|
container_name: mcworldgen
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "25565:25565"
|
||||||
|
environment:
|
||||||
|
EULA: "TRUE"
|
||||||
|
VERSION: "1.21.11"
|
||||||
|
TYPE: "PAPER"
|
||||||
|
LEVEL: "world"
|
||||||
|
MEMORY: "8G"
|
||||||
|
USE_AIKAR_FLAGS: "true"
|
||||||
|
ENABLE_RCON: "true"
|
||||||
|
RCON_PASSWORD: "mcworldgen"
|
||||||
|
VIEW_DISTANCE: "10"
|
||||||
|
SIMULATION_DISTANCE: "6"
|
||||||
|
MOTD: "MC Worldgen test map"
|
||||||
|
GENERATE_STRUCTURES: "false"
|
||||||
|
volumes:
|
||||||
|
- ./data:/data
|
||||||
Reference in New Issue
Block a user