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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ The plugin also supplies the shared economy used by other TF-Minecraft systems,
- **Multiple denominations** — represent values with gold and silver coins, handfuls, stacks, and pouches of coins.
- **Coins in the world** — drop money from your pouch as physical coins for others to collect.
- **Balance visibility** — inspect personal balances and view a leaderboard of the wealthiest accounts.
- **Admin credits** — console or operators can add denars to a player's bank or pouch with `/deco give <player> <amount> [bank|pouch]`, whether the player is online or offline (permission `denareconomy.give`, op by default).
- **Shared earnings** — give other plugins a common way to award money and respond to income, banking, and material deposits.

## Documentation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.logging.Level;
import java.util.stream.Collectors;

import org.bukkit.Bukkit;
Expand All @@ -16,10 +18,12 @@

import net.tfminecraft.tlibs.utils.ParseUtils;
import net.tfminecraft.denareconomy.DenarEconomy;
import net.tfminecraft.denareconomy.accounts.OfflineModifier;
import net.tfminecraft.denareconomy.data.Account;
import net.tfminecraft.denareconomy.data.PlayerData;
import net.tfminecraft.denareconomy.database.BalTopEntry;
import net.tfminecraft.denareconomy.database.Database;
import net.tfminecraft.denareconomy.enums.Accounts;
import net.tfminecraft.denareconomy.item.Coin;
import net.tfminecraft.denareconomy.loaders.CoinLoader;
import net.tfminecraft.denareconomy.loaders.MessageLoader;
Expand All @@ -40,6 +44,12 @@ public boolean onCommand(CommandSender sender, Command cmd, String label, String
return true;
}

if (cmd.getName().equalsIgnoreCase(cmd1) && args.length > 0
&& args[0].equalsIgnoreCase("give")) {
handleGive(sender, args);
return true;
}

if (!(sender instanceof Player)) {
MessageLoader.send(sender, "general.players-only");
return false;
Expand Down Expand Up @@ -96,11 +106,84 @@ private void handleReload(CommandSender sender) {
}

private static boolean canReload(CommandSender sender) {
return isAdmin(sender, "denareconomy.reload");
}

private static boolean canGive(CommandSender sender) {
return isAdmin(sender, "denareconomy.give");
}

/** Console always passes; a player needs operator status or the permission. */
private static boolean isAdmin(CommandSender sender, String permission) {
if (!(sender instanceof Player)) {
return true;
}
Player player = (Player) sender;
return player.isOp() || player.hasPermission("denareconomy.reload");
return player.isOp() || player.hasPermission(permission);
}

/**
* Credit an account from console or by an operator: /deco give <player> <amount> [bank|pouch].
* The bank is the default. Offline players are paid through their saved account.
*/
private void handleGive(CommandSender sender, String[] args) {
if (!canGive(sender)) {
MessageLoader.send(sender, "errors.no-permission");
return;
}
if (args.length < 3 || args.length > 4) {
MessageLoader.send(sender, "errors.give-usage");
return;
}

BigDecimal amount = parseBankAmount(args[2]);
if (amount == null) {
MessageLoader.send(sender, "errors.invalid-amount");
return;
}

Accounts account = args.length == 4 ? parseAccount(args[3]) : Accounts.BANK;
if (account == null) {
MessageLoader.send(sender, "errors.give-usage");
return;
}

String name = args[1];
UUID id = OfflineModifier.playerId(name);
if (id == null) {
MessageLoader.send(sender, "errors.unknown-player", "player", name);
return;
}

String accountName = account.name().toLowerCase();
boolean paid;
try {
paid = OfflineModifier.apply(id, account, amount.doubleValue());
} catch (RuntimeException failure) {
DenarEconomy.plugin.getLogger().log(Level.SEVERE, "Could not give " + amount + " to " + name
+ " (" + id + ")", failure);
paid = false;
}
if (!paid) {
MessageLoader.send(sender, "errors.give-failed", "player", name);
return;
}

DenarEconomy.plugin.getLogger().info(sender.getName() + " gave " + amount + " to " + name
+ " (" + id + ") " + accountName);
MessageLoader.send(sender, "give.sent", "amount", amount, "player", name, "account", accountName);
Player target = Bukkit.getPlayer(id);
if (target != null) {
MessageLoader.send(target, "give.received", "amount", amount, "account", accountName);
}
}

private static Accounts parseAccount(String token) {
return switch (token.toLowerCase()) {
case "bank" -> Accounts.BANK;
case "pouch" -> Accounts.POUCH;
default -> null;
};
}

private void handleBalTop(Player p) {
Expand Down Expand Up @@ -288,6 +371,15 @@ private void sendBankReport(Player p, String action, BigDecimal amount, PlayerDa
p.playSound(p, Sound.BLOCK_NOTE_BLOCK_CHIME, 1f, 1f);
}

private static List<String> giveCompletions(int position) {
return switch (position) {
case 2 -> List.of("<player>");
case 3 -> List.of("<amount>");
case 4 -> List.of("bank", "pouch");
default -> List.of();
};
}

private void sendError(Player p) {
MessageLoader.send(p, "general.unknown-subcommand");
}
Expand All @@ -303,9 +395,16 @@ public List<String> onTabComplete(CommandSender sender, Command cmd, String alia
completions.add("deposit");
completions.add("withdraw");
completions.add("baltop");
if (canGive(sender)) {
completions.add("give");
}
if (canReload(sender)) {
completions.add("reload");
}
} else if (args.length > 1 && args[0].equalsIgnoreCase("give")) {
if (canGive(sender)) {
completions.addAll(giveCompletions(args.length));
}
} else if (args.length == 2) {
if (args[0].equalsIgnoreCase("pay") || args[0].equalsIgnoreCase("toitem") ||
args[0].equalsIgnoreCase("deposit") || args[0].equalsIgnoreCase("withdraw")) {
Expand Down
7 changes: 7 additions & 0 deletions src/main/resources/messages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ errors:
unknown-coin: "%prefix%&cUnknown coin."
invalid-count: "%prefix%&cAmount must be a whole number of coins."
coins-unavailable: "%prefix%&cCannot create coins for this amount. Your pouch has not been charged."
give-usage: "%prefix%&cUsage: /deco give <player> <amount> [bank|pouch]"
unknown-player: "%prefix%&cNo denar account is known for %player%."
give-failed: "%prefix%&cCould not pay %player%. Their balance has not changed."

balance:
pouch: "#3ce8c9Current Pouch Balance: #b39122%amount%#dbaf1dd"
Expand All @@ -43,6 +46,10 @@ bank:
deposited: "Deposited"
withdrew: "Withdrew"

give:
sent: "%prefix%&aGave #b39122%amount%#dbaf1dd &ato %player%'s %account%."
received: "#dbaf1d+#b39122%amount%#dbaf1dd #3ce8c9added to your %account%."

money:
earned: "#dbaf1d+#b39122%amount%#dbaf1dd"
tax: "#44524f(%tax% in tax)"
Expand Down
3 changes: 3 additions & 0 deletions src/main/resources/plugin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,7 @@ commands:
permissions:
denareconomy.reload:
description: Reload DenarEconomy configs
default: op
denareconomy.give:
description: Credit a player's bank or pouch with /deco give
default: op
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@
import java.math.BigDecimal;
import java.util.List;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.tfminecraft.denareconomy.DenarEconomy;
import net.tfminecraft.denareconomy.accounts.OfflineModifier;
import net.tfminecraft.denareconomy.data.PlayerData;
import net.tfminecraft.denareconomy.database.BalTopEntry;
import net.tfminecraft.denareconomy.database.Database;
import net.tfminecraft.denareconomy.enums.Accounts;
import net.tfminecraft.denareconomy.event.PlayerBankPulseEvent;
import net.tfminecraft.denareconomy.item.Coin;
import net.tfminecraft.denareconomy.loaders.CoinLoader;
Expand Down Expand Up @@ -317,6 +321,161 @@ void completionsFilterCaseInsensitivelyAndRespectReloadPermission() {
List.of(), commands.onTabComplete(player, command("other"), "other", new String[] {""}));
}

@Test
void giveRejectsPlayersWithoutOperatorStatusOrPermission() {
try (MockedStatic<OfflineModifier> offline = mockStatic(OfflineModifier.class)) {
assertTrue(run("give", "Alex", "5"));
messages.verify(() -> MessageLoader.send(player, "errors.no-permission"));
offline.verifyNoInteractions();
}
}

@Test
void giveValidatesUsageAmountAccountAndPlayer() {
CommandSender console = mock(CommandSender.class);
try (MockedStatic<OfflineModifier> offline = mockStatic(OfflineModifier.class)) {
give(console);
give(console, "Alex");
give(console, "Alex", "5", "bank", "extra");
give(console, "Alex", "5", "wallet");
messages.verify(() -> MessageLoader.send(console, "errors.give-usage"), times(4));
give(console, "Alex", "0.005");
give(console, "Alex", "-1");
messages.verify(() -> MessageLoader.send(console, "errors.invalid-amount"), times(2));
give(console, "Nobody", "5");
messages.verify(
() -> MessageLoader.send(console, "errors.unknown-player", "player", "Nobody"));
offline.verify(() -> OfflineModifier.apply(any(UUID.class), any(), anyDouble()), never());
}
}

@Test
void giveCreditsTheBankByDefaultAndThePouchOnRequest() {
Logger logger = pluginLogger();
CommandSender console = mock(CommandSender.class);
when(console.getName()).thenReturn("CONSOLE");
UUID id = UUID.randomUUID();
try (MockedStatic<OfflineModifier> offline = mockStatic(OfflineModifier.class)) {
offline.when(() -> OfflineModifier.playerId("Alex")).thenReturn(id);
offline.when(() -> OfflineModifier.apply(eq(id), any(), anyDouble())).thenReturn(true);
give(console, "Alex", "12.5");
give(console, "Alex", "0.02", "POUCH");
give(console, "Alex", "3", "Bank");
offline.verify(() -> OfflineModifier.apply(id, Accounts.BANK, 12.5));
offline.verify(() -> OfflineModifier.apply(id, Accounts.POUCH, 0.02));
offline.verify(() -> OfflineModifier.apply(id, Accounts.BANK, 3.0));
messages.verify(
() ->
MessageLoader.send(
console,
"give.sent",
"amount",
new BigDecimal("12.50"),
"player",
"Alex",
"account",
"bank"));
messages.verify(
() ->
MessageLoader.send(
console,
"give.sent",
"amount",
new BigDecimal("0.02"),
"player",
"Alex",
"account",
"pouch"));
verify(logger).info("CONSOLE gave 12.50 to Alex (" + id + ") bank");
verify(logger).info("CONSOLE gave 0.02 to Alex (" + id + ") pouch");
// Alex is offline, so only the sender hears about it.
messages.verify(() -> MessageLoader.send(any(Player.class), eq("give.received"), any()), never());
}
}

@Test
void giveTellsAnOnlineRecipientAndAcceptsOperatorsAndPermissionHolders() {
pluginLogger();
UUID id = UUID.randomUUID();
Player target = mock(Player.class);
bukkit.when(() -> Bukkit.getPlayer(id)).thenReturn(target);
when(player.getName()).thenReturn("Admin");
try (MockedStatic<OfflineModifier> offline = mockStatic(OfflineModifier.class)) {
offline.when(() -> OfflineModifier.playerId("Sam")).thenReturn(id);
offline.when(() -> OfflineModifier.apply(eq(id), any(), anyDouble())).thenReturn(true);
when(player.isOp()).thenReturn(true);
assertTrue(run("give", "Sam", "4"));
when(player.isOp()).thenReturn(false);
when(player.hasPermission("denareconomy.give")).thenReturn(true);
assertTrue(run("GIVE", "Sam", "1.25", "pouch"));
offline.verify(() -> OfflineModifier.apply(id, Accounts.BANK, 4.0));
offline.verify(() -> OfflineModifier.apply(id, Accounts.POUCH, 1.25));
messages.verify(
() ->
MessageLoader.send(
target, "give.received", "amount", new BigDecimal("4.00"), "account", "bank"));
messages.verify(
() ->
MessageLoader.send(
target, "give.received", "amount", new BigDecimal("1.25"), "account", "pouch"));
}
}

@Test
void giveReportsRefusedAndFailedPaymentsWithoutClaimingSuccess() {
Logger logger = pluginLogger();
CommandSender console = mock(CommandSender.class);
UUID refused = UUID.randomUUID();
UUID broken = UUID.randomUUID();
IllegalStateException failure = new IllegalStateException("disk full");
try (MockedStatic<OfflineModifier> offline = mockStatic(OfflineModifier.class)) {
offline.when(() -> OfflineModifier.playerId("Alex")).thenReturn(refused);
offline.when(() -> OfflineModifier.playerId("Sam")).thenReturn(broken);
offline.when(() -> OfflineModifier.apply(eq(refused), any(), anyDouble())).thenReturn(false);
offline
.when(() -> OfflineModifier.apply(eq(broken), any(), anyDouble()))
.thenThrow(failure);
give(console, "Alex", "5");
give(console, "Sam", "5");
messages.verify(() -> MessageLoader.send(console, "errors.give-failed", "player", "Alex"));
messages.verify(() -> MessageLoader.send(console, "errors.give-failed", "player", "Sam"));
verify(logger)
.log(Level.SEVERE, "Could not give 5.00 to Sam (" + broken + ")", failure);
verify(logger, never()).info(anyString());
messages.verify(
() -> MessageLoader.send(eq(console), eq("give.sent"), any(Object[].class)), never());
}
}

@Test
void giveCompletionsNeedPermissionAndSuggestEachArgument() {
CommandSender console = mock(CommandSender.class);
assertTrue(
commands.onTabComplete(console, deco, "deco", new String[] {"g"}).contains("give"));
assertEquals(List.of(), tab("give", ""));
when(player.hasPermission("denareconomy.give")).thenReturn(true);
assertEquals(List.of("give"), tab("gi"));
assertEquals(List.of("<player>"), tab("give", ""));
assertEquals(List.of("<amount>"), tab("give", "Alex", ""));
assertEquals(List.of("bank", "pouch"), tab("give", "Alex", "5", ""));
assertEquals(List.of("pouch"), tab("Give", "Alex", "5", "P"));
assertEquals(List.of(), tab("give", "Alex", "5", "bank", ""));
}

private void give(CommandSender sender, String... args) {
String[] full = new String[args.length + 1];
full[0] = "give";
System.arraycopy(args, 0, full, 1, args.length);
assertTrue(commands.onCommand(sender, deco, "deco", full));
}

private Logger pluginLogger() {
DenarEconomy.plugin = mock(DenarEconomy.class);
Logger logger = mock(Logger.class);
when(DenarEconomy.plugin.getLogger()).thenReturn(logger);
return logger;
}

private boolean run(String... args) {
return commands.onCommand(player, deco, "deco", args);
}
Expand Down
Loading