Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
public record EconomyConfig(
boolean enabled,
@NotNull String provider,
@NotNull ItemEconomyConfig item,
double anyHeadPrice,
@NotNull Map<String, Double> categoryPrices,
@NotNull Map<String, Double> customCategoryPrices,
Expand All @@ -27,6 +28,7 @@ public record EconomyConfig(

public EconomyConfig {
Objects.requireNonNull(provider, "provider");
Objects.requireNonNull(item, "item");
Objects.requireNonNull(categoryPrices, "categoryPrices");
Objects.requireNonNull(customCategoryPrices, "customCategoryPrices");
Objects.requireNonNull(headPrices, "headPrices");
Expand Down Expand Up @@ -55,6 +57,12 @@ public record EconomyConfig(
return new EconomyConfig(
yaml.getBoolean("enabled", false),
yaml.getString("provider", "vault"),
new ItemEconomyConfig(
yaml.getString("item.match", ItemEconomyProvider.MATCH_MATERIAL),
yaml.getString("item.material", "DIAMOND"),
yaml.getString("item.singular", ""),
yaml.getString("item.plural", "")
),
yaml.getDouble("prices.any-head", 0.0D),
prices(yaml.getConfigurationSection("prices.categories")),
prices(yaml.getConfigurationSection("prices.custom-categories")),
Expand Down Expand Up @@ -144,10 +152,19 @@ private static void copyDefault(@NotNull Path file) {

private static @NotNull String defaultConfig() {
return "enabled: false\n"
+ "# Supported providers: vault, item\n"
+ "provider: vault\n"
+ "\n"
+ "item:\n"
+ " # Item economy currently supports material-only matching.\n"
+ " match: material\n"
+ " material: DIAMOND\n"
+ " singular: Diamond\n"
+ " plural: Diamonds\n"
+ "\n"
+ "prices:\n"
+ " # Fallback price for every head. Set to 0 to make heads free.\n"
+ " # Item economy prices must be whole numbers.\n"
+ " any-head: 0.0\n"
+ "\n"
+ " # Price for player heads. If omitted, any-head is used.\n"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import io.github.silentdevelopment.headdb.model.Head;
import io.github.silentdevelopment.headdb.paper.HeadDBPlugin;
import net.milkbowl.vault.economy.Economy;
import org.bukkit.Material;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.plugin.RegisteredServiceProvider;
Expand Down Expand Up @@ -36,25 +37,113 @@ private EconomyService(@NotNull HeadDBPlugin plugin, @NotNull EconomyConfig conf
return new EconomyService(plugin, config, new DisabledEconomyProvider());
}

if (!config.provider().equalsIgnoreCase("vault")) {
plugin.getSLF4JLogger().warn("Unsupported economy provider '{}'. Economy integration is disabled.", config.provider());
return new EconomyService(plugin, config, new DisabledEconomyProvider());
}
return switch (config.provider()) {
case "vault" -> createVault(plugin, config);
case "item" -> createItem(plugin, config);
default -> {
plugin.getSLF4JLogger().warn("Unsupported economy provider '{}'. Economy integration is disabled.", config.provider());
yield new EconomyService(plugin, config, new DisabledEconomyProvider());
}
};
}

private static @NotNull EconomyService createVault(@NotNull HeadDBPlugin plugin, @NotNull EconomyConfig config) {
if (plugin.getServer().getPluginManager().getPlugin("Vault") == null) {
plugin.getSLF4JLogger().warn("Economy is enabled but Vault is not loaded.");
plugin.getSLF4JLogger().warn("Economy is enabled with provider 'vault' but Vault is not loaded.");
return new EconomyService(plugin, config, new DisabledEconomyProvider());
}

RegisteredServiceProvider<Economy> registration = plugin.getServer().getServicesManager().getRegistration(Economy.class);
if (registration == null || registration.getProvider() == null) {
plugin.getSLF4JLogger().warn("Economy is enabled but Vault did not provide an Economy service.");
plugin.getSLF4JLogger().warn("Economy is enabled with provider 'vault' but Vault did not provide an Economy service.");
return new EconomyService(plugin, config, new DisabledEconomyProvider());
}

return new EconomyService(plugin, config, new VaultEconomyProvider(registration.getProvider()));
}

private static @NotNull EconomyService createItem(@NotNull HeadDBPlugin plugin, @NotNull EconomyConfig config) {
ItemEconomyConfig itemConfig = config.item();

if (!itemConfig.match().equals(ItemEconomyProvider.MATCH_MATERIAL)) {
plugin.getSLF4JLogger().warn(
"Unsupported item economy match mode '{}'. Only 'material' is supported. Economy integration is disabled.",
itemConfig.match()
);
return new EconomyService(plugin, config, new DisabledEconomyProvider());
}

Material material = Material.matchMaterial(itemConfig.material());
if (material == null || material.isAir() || !material.isItem()) {
plugin.getSLF4JLogger().warn(
"Invalid item economy material '{}'. Economy integration is disabled.",
itemConfig.material()
);
return new EconomyService(plugin, config, new DisabledEconomyProvider());
}

if (!wholeItemPrices(config)) {
plugin.getSLF4JLogger().warn(
"Item economy requires whole-number prices. Economy integration is disabled until fractional prices are removed."
);
return new EconomyService(plugin, config, new DisabledEconomyProvider());
}

String fallbackName = displayName(material);
String singular = itemConfig.singularOr(fallbackName);
String plural = itemConfig.pluralOr(singular + "s");
return new EconomyService(plugin, config, new ItemEconomyProvider(material, singular, plural));
}

private static boolean wholeItemPrices(@NotNull EconomyConfig config) {
if (!ItemEconomyProvider.isWholeAmount(config.anyHeadPrice())) {
return false;
}

if (config.playerHeadPrice() != null && !ItemEconomyProvider.isWholeAmount(config.playerHeadPrice())) {
return false;
}

for (double price : config.categoryPrices().values()) {
if (!ItemEconomyProvider.isWholeAmount(price)) {
return false;
}
}

for (double price : config.customCategoryPrices().values()) {
if (!ItemEconomyProvider.isWholeAmount(price)) {
return false;
}
}

for (double price : config.headPrices().values()) {
if (!ItemEconomyProvider.isWholeAmount(price)) {
return false;
}
}

return true;
}

private static @NotNull String displayName(@NotNull Material material) {
String[] parts = material.name().toLowerCase(Locale.ROOT).split("_");
StringBuilder result = new StringBuilder();

for (String part : parts) {
if (part.isBlank()) {
continue;
}

if (!result.isEmpty()) {
result.append(' ');
}

result.append(Character.toUpperCase(part.charAt(0))).append(part.substring(1));
}

return result.toString();
}

public boolean enabled() {
return config.enabled() && provider.available();
}
Expand Down Expand Up @@ -137,8 +226,12 @@ public void setCustomCategoryPrice(@NotNull String categoryId, double price) {
}

private void setPrice(@NotNull String path, double price) {
if (price < 0.0D) {
throw new IllegalArgumentException("Price cannot be negative.");
if (!Double.isFinite(price) || price < 0.0D) {
throw new IllegalArgumentException("Price must be a finite non-negative number.");
}

if (config.provider().equals("item") && !ItemEconomyProvider.isWholeAmount(price)) {
throw new IllegalArgumentException("Item economy prices must be whole numbers.");
}

Path file = plugin.getDataFolder().toPath().resolve("economy.yml");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package io.github.silentdevelopment.headdb.paper.economy;

import org.jetbrains.annotations.NotNull;

import java.util.Locale;
import java.util.Objects;

public record ItemEconomyConfig(
@NotNull String match,
@NotNull String material,
@NotNull String singular,
@NotNull String plural
) {

public ItemEconomyConfig {
Objects.requireNonNull(match, "match");
Objects.requireNonNull(material, "material");
Objects.requireNonNull(singular, "singular");
Objects.requireNonNull(plural, "plural");

match = match.trim().toLowerCase(Locale.ROOT);
material = material.trim();
singular = singular.trim();
plural = plural.trim();
}

public @NotNull String singularOr(@NotNull String fallback) {
Objects.requireNonNull(fallback, "fallback");
return singular.isBlank() ? fallback : singular;
}

public @NotNull String pluralOr(@NotNull String fallback) {
Objects.requireNonNull(fallback, "fallback");
return plural.isBlank() ? fallback : plural;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package io.github.silentdevelopment.headdb.paper.economy;

import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Locale;
import java.util.Objects;

public final class ItemEconomyProvider implements EconomyProvider {

public static final String MATCH_MATERIAL = "material";

private final Material material;
private final String singular;
private final String plural;

public ItemEconomyProvider(@NotNull Material material, @NotNull String singular, @NotNull String plural) {
this.material = Objects.requireNonNull(material, "material");
this.singular = Objects.requireNonNull(singular, "singular");
this.plural = Objects.requireNonNull(plural, "plural");
}

@Override
public boolean available() {
return true;
}

@Override
public @NotNull String format(double amount) {
Long units = wholeUnits(amount);
if (units == null) {
return String.format(Locale.ROOT, "%.2f %s", amount, plural);
}

return units + " " + (units == 1L ? singular : plural);
}

@Override
public @NotNull EconomyWithdrawal withdraw(@NotNull Player player, double amount) {
Objects.requireNonNull(player, "player");

Long required = wholeUnits(amount);
if (required == null) {
return new EconomyWithdrawal(false, "Item economy requires whole-number prices.");
}

if (required <= 0L) {
return new EconomyWithdrawal(true, "");
}

PlayerInventory inventory = player.getInventory();
ItemStack[] updated = withdrawStorage(inventory.getStorageContents(), material, required);
if (updated == null) {
return new EconomyWithdrawal(false, "Insufficient item currency.");
}

inventory.setStorageContents(updated);
return new EconomyWithdrawal(true, "");
}

static boolean isWholeAmount(double amount) {
return wholeUnits(amount) != null;
}

static @Nullable ItemStack[] withdrawStorage(@NotNull ItemStack[] storage, @NotNull Material material, long required) {
Objects.requireNonNull(storage, "storage");
Objects.requireNonNull(material, "material");

if (required < 0L) {
throw new IllegalArgumentException("Required amount cannot be negative.");
}

int[] matchingAmounts = new int[storage.length];
for (int index = 0; index < storage.length; index++) {
ItemStack item = storage[index];
if (item != null && item.getType() == material) {
matchingAmounts[index] = item.getAmount();
}
}

int[] remainingAmounts = withdrawAmounts(matchingAmounts, required);
if (remainingAmounts == null) {
return null;
}

ItemStack[] updated = new ItemStack[storage.length];
for (int index = 0; index < storage.length; index++) {
ItemStack item = storage[index];
updated[index] = item == null ? null : item.clone();

if (item == null || item.getType() != material) {
continue;
}

int remaining = remainingAmounts[index];
if (remaining <= 0) {
updated[index] = null;
} else {
updated[index].setAmount(remaining);
}
}

return updated;
}

static @Nullable int[] withdrawAmounts(@NotNull int[] amounts, long required) {
Objects.requireNonNull(amounts, "amounts");

if (required < 0L) {
throw new IllegalArgumentException("Required amount cannot be negative.");
}

long available = 0L;
for (int amount : amounts) {
if (amount < 0) {
throw new IllegalArgumentException("Stack amount cannot be negative.");
}

available += amount;
if (available >= required) {
break;
}
}

if (available < required) {
return null;
}

int[] updated = amounts.clone();
long remaining = required;

for (int index = 0; index < updated.length && remaining > 0L; index++) {
int remove = (int) Math.min(remaining, updated[index]);
updated[index] -= remove;
remaining -= remove;
}

return updated;
}

private static @Nullable Long wholeUnits(double amount) {
if (!Double.isFinite(amount) || amount < 0.0D || amount != Math.rint(amount)) {
return null;
}

if (amount > Long.MAX_VALUE) {
return null;
}

return (long) amount;
}
}
Loading
Loading